nextjs-rewrite #5
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Complete < EuchreCamp::Action
|
||||||
|
include Deps[events_repo: 'repositories.events']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
|
||||||
|
events_repo.update(tournament_id, status: 'completed')
|
||||||
|
|
||||||
|
response.flash[:message] = "Tournament completed"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to complete tournament: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Create < EuchreCamp::Action
|
||||||
|
include Deps[repo: 'repositories.events']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
params = request.params.to_h
|
||||||
|
|
||||||
|
event_params = {
|
||||||
|
name: params[:name],
|
||||||
|
description: params[:description],
|
||||||
|
event_date: params[:event_date] ? Time.parse(params[:event_date]) : nil,
|
||||||
|
event_type: 'tournament',
|
||||||
|
format: params[:format] || 'single_elim',
|
||||||
|
status: 'planned',
|
||||||
|
max_participants: params[:max_participants]&.to_i
|
||||||
|
}
|
||||||
|
|
||||||
|
event = repo.create(event_params)
|
||||||
|
|
||||||
|
response.redirect_to("/admin/tournaments/#{event[:id]}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to create tournament: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/new")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Destroy < EuchreCamp::Action
|
||||||
|
include Deps[repo: 'repositories.events']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
repo.delete(tournament_id)
|
||||||
|
response.flash[:message] = "Tournament deleted"
|
||||||
|
response.redirect_to("/admin/tournaments")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to delete: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Edit < EuchreCamp::Action
|
||||||
|
include Deps[repo: 'repositories.events']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
tournament = repo.by_id(tournament_id)
|
||||||
|
response.render(view, tournament: tournament)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Index < EuchreCamp::Action
|
||||||
|
include Deps[repo: 'repositories.events']
|
||||||
|
|
||||||
|
def handle(_request, response)
|
||||||
|
upcoming = repo.upcoming
|
||||||
|
completed = repo.completed
|
||||||
|
response.render(view, upcoming: upcoming, completed: completed)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class New < EuchreCamp::Action
|
||||||
|
def handle(_request, response)
|
||||||
|
response.render(view)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
module Participants
|
||||||
|
class Create < EuchreCamp::Action
|
||||||
|
include Deps[participants_repo: 'repositories.event_participants']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
player_id = request.params[:player_id]
|
||||||
|
|
||||||
|
if player_id.nil? || player_id.empty?
|
||||||
|
response.flash[:error] = "Please select a player"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
participants_repo.register_player(tournament_id, player_id.to_i)
|
||||||
|
|
||||||
|
response.flash[:message] = "Player added to tournament"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to add player: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
module Participants
|
||||||
|
class Destroy < EuchreCamp::Action
|
||||||
|
include Deps[participants_repo: 'repositories.event_participants']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
player_id = request.params[:player_id]
|
||||||
|
|
||||||
|
participants_repo.remove_player(tournament_id, player_id.to_i)
|
||||||
|
|
||||||
|
response.flash[:message] = "Player removed from tournament"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to remove player: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Results < EuchreCamp::Action
|
||||||
|
include Deps[
|
||||||
|
events_repo: 'repositories.events',
|
||||||
|
rounds_repo: 'repositories.tournament_rounds',
|
||||||
|
matchups_repo: 'repositories.bracket_matchups',
|
||||||
|
teams_repo: 'repositories.teams'
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
|
||||||
|
tournament = events_repo.by_id(tournament_id)
|
||||||
|
current_round = rounds_repo.current_round(tournament_id)
|
||||||
|
|
||||||
|
if current_round.nil?
|
||||||
|
response.flash[:error] = "No active round to enter results"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
matchups = matchups_repo.by_round(current_round[:id])
|
||||||
|
teams = teams_repo.by_event(tournament_id)
|
||||||
|
|
||||||
|
response.render(view,
|
||||||
|
tournament: tournament,
|
||||||
|
round: current_round,
|
||||||
|
matchups: matchups,
|
||||||
|
teams: teams
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
module Rounds
|
||||||
|
class Show < EuchreCamp::Action
|
||||||
|
include Deps[
|
||||||
|
events_repo: 'repositories.events',
|
||||||
|
rounds_repo: 'repositories.tournament_rounds',
|
||||||
|
matchups_repo: 'repositories.bracket_matchups',
|
||||||
|
teams_repo: 'repositories.teams'
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
round_id = request.params[:round_id]
|
||||||
|
|
||||||
|
tournament = events_repo.by_id(tournament_id)
|
||||||
|
round = rounds_repo.by_event(tournament_id).find { |r| r[:id] == round_id.to_i }
|
||||||
|
matchups = matchups_repo.by_round(round_id)
|
||||||
|
teams = teams_repo.by_event(tournament_id)
|
||||||
|
|
||||||
|
response.render(view,
|
||||||
|
tournament: tournament,
|
||||||
|
round: round,
|
||||||
|
matchups: matchups,
|
||||||
|
teams: teams
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class SaveResults < EuchreCamp::Action
|
||||||
|
include Deps[
|
||||||
|
events_repo: 'repositories.events',
|
||||||
|
rounds_repo: 'repositories.tournament_rounds',
|
||||||
|
matchups_repo: 'repositories.bracket_matchups',
|
||||||
|
teams_repo: 'repositories.teams',
|
||||||
|
matches_repo: 'repositories.matches'
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
params = request.params.to_h
|
||||||
|
|
||||||
|
# Process each matchup result
|
||||||
|
params.each do |key, value|
|
||||||
|
key_str = key.to_s
|
||||||
|
if key_str.start_with?('matchup_')
|
||||||
|
matchup_id = key_str.gsub('matchup_', '').to_i
|
||||||
|
team_1_score = params["team_1_score_#{matchup_id}"]&.to_i || 0
|
||||||
|
team_2_score = params["team_2_score_#{matchup_id}"]&.to_i || 0
|
||||||
|
|
||||||
|
matchup = matchups_repo.bracket_matchups.by_pk(matchup_id).one
|
||||||
|
unless matchup && matchup[:team_1_id] && matchup[:team_2_id]
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create match record
|
||||||
|
team_1 = teams_repo.teams.by_pk(matchup[:team_1_id]).one
|
||||||
|
team_2 = teams_repo.teams.by_pk(matchup[:team_2_id]).one
|
||||||
|
|
||||||
|
next if team_1.nil? || team_2.nil?
|
||||||
|
|
||||||
|
match = matches_repo.create(
|
||||||
|
event_id: tournament_id,
|
||||||
|
team_1_p1_id: team_1.player_1_id,
|
||||||
|
team_1_p2_id: team_1.player_2_id,
|
||||||
|
team_2_p1_id: team_2.player_1_id,
|
||||||
|
team_2_p2_id: team_2.player_2_id,
|
||||||
|
team_1_score: team_1_score,
|
||||||
|
team_2_score: team_2_score,
|
||||||
|
status: 'completed'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update matchup with match and winner
|
||||||
|
winner_id = team_1_score > team_2_score ? matchup[:team_1_id] : matchup[:team_2_id]
|
||||||
|
loser_id = team_1_score > team_2_score ? matchup[:team_2_id] : matchup[:team_1_id]
|
||||||
|
|
||||||
|
matchups_repo.update(matchup_id,
|
||||||
|
match_id: match[:id],
|
||||||
|
winner_team_id: winner_id,
|
||||||
|
loser_team_id: loser_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enqueue Elo calculation
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
response.flash[:message] = "Results saved"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to save results: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}/results")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Schedule < EuchreCamp::Action
|
||||||
|
include Deps[
|
||||||
|
events_repo: 'repositories.events',
|
||||||
|
teams_repo: 'repositories.teams',
|
||||||
|
rounds_repo: 'repositories.tournament_rounds',
|
||||||
|
matchups_repo: 'repositories.bracket_matchups'
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
tournament = events_repo.by_id(tournament_id)
|
||||||
|
teams = teams_repo.by_event(tournament_id)
|
||||||
|
|
||||||
|
if teams.empty?
|
||||||
|
response.flash[:error] = "No teams to schedule. Add teams first."
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate matchups based on format
|
||||||
|
teams_data = teams.map { |t| { id: t[:id], seed: t[:id] } }
|
||||||
|
format = tournament[:format]
|
||||||
|
|
||||||
|
begin
|
||||||
|
case format
|
||||||
|
when 'round_robin'
|
||||||
|
rounds = EuchreCamp::Services::TournamentGenerator.round_robin(teams_data)
|
||||||
|
create_round_robin_schedule(tournament_id, rounds)
|
||||||
|
when 'single_elim'
|
||||||
|
bracket = EuchreCamp::Services::TournamentGenerator.single_elimination(teams_data)
|
||||||
|
create_single_elim_schedule(tournament_id, bracket, teams)
|
||||||
|
when 'swiss'
|
||||||
|
# For Swiss, create first round
|
||||||
|
rounds = EuchreCamp::Services::TournamentGenerator.swiss(teams_data, 1)
|
||||||
|
create_swiss_schedule(tournament_id, rounds, 1)
|
||||||
|
else
|
||||||
|
response.flash[:error] = "Format '#{format}' not yet implemented"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update tournament status
|
||||||
|
events_repo.update(tournament_id, status: 'active')
|
||||||
|
|
||||||
|
response.flash[:message] = "Schedule generated successfully"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to generate schedule: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_round_robin_schedule(tournament_id, rounds)
|
||||||
|
rounds.each do |round_data|
|
||||||
|
round = rounds_repo.create(
|
||||||
|
event_id: tournament_id,
|
||||||
|
round_number: round_data[:round_number],
|
||||||
|
status: round_data[:round_number] == 1 ? 'active' : 'pending'
|
||||||
|
)
|
||||||
|
|
||||||
|
round_data[:matchups].each_with_index do |matchup, index|
|
||||||
|
matchups_repo.create(
|
||||||
|
event_id: tournament_id,
|
||||||
|
round_id: round[:id],
|
||||||
|
team_1_id: matchup[:team_1_id],
|
||||||
|
team_2_id: matchup[:team_2_id],
|
||||||
|
bracket_position: index + 1
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_single_elim_schedule(tournament_id, bracket, teams)
|
||||||
|
# Create first round
|
||||||
|
round = rounds_repo.create(
|
||||||
|
event_id: tournament_id,
|
||||||
|
round_number: 1,
|
||||||
|
status: 'active'
|
||||||
|
)
|
||||||
|
|
||||||
|
bracket[:first_round_matchups].each do |matchup|
|
||||||
|
# Skip if team is nil (BYE)
|
||||||
|
next if matchup[:team_1_id].nil? && matchup[:team_2_id].nil?
|
||||||
|
|
||||||
|
matchups_repo.create(
|
||||||
|
event_id: tournament_id,
|
||||||
|
round_id: round[:id],
|
||||||
|
team_1_id: matchup[:team_1_id],
|
||||||
|
team_2_id: matchup[:team_2_id],
|
||||||
|
bracket_position: matchup[:bracket_position]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_swiss_schedule(tournament_id, rounds, round_number)
|
||||||
|
round = rounds_repo.create(
|
||||||
|
event_id: tournament_id,
|
||||||
|
round_number: round_number,
|
||||||
|
status: 'active'
|
||||||
|
)
|
||||||
|
|
||||||
|
rounds.each do |round_data|
|
||||||
|
round_data[:matchups].each_with_index do |matchup, index|
|
||||||
|
matchups_repo.create(
|
||||||
|
event_id: tournament_id,
|
||||||
|
round_id: round[:id],
|
||||||
|
team_1_id: matchup[:team_1_id],
|
||||||
|
team_2_id: matchup[:team_2_id],
|
||||||
|
bracket_position: index + 1
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Show < EuchreCamp::Action
|
||||||
|
include Deps[
|
||||||
|
events_repo: 'repositories.events',
|
||||||
|
participants_repo: 'repositories.event_participants',
|
||||||
|
teams_repo: 'repositories.teams',
|
||||||
|
rounds_repo: 'repositories.tournament_rounds',
|
||||||
|
players_repo: 'repositories.players'
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
tournament = events_repo.by_id(tournament_id)
|
||||||
|
|
||||||
|
# Get all registered participants for this tournament
|
||||||
|
participants = participants_repo.by_event(tournament_id)
|
||||||
|
participant_ids = participants.map { |p| p[:player_id] }
|
||||||
|
|
||||||
|
# Get registered players details
|
||||||
|
players = if participant_ids.any?
|
||||||
|
players_repo.players.where(id: participant_ids).to_a
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get ALL players for the registration dropdown (not yet registered)
|
||||||
|
all_players = players_repo.all
|
||||||
|
available_players = all_players.reject { |p| participant_ids.include?(p[:id]) }
|
||||||
|
|
||||||
|
# Get teams
|
||||||
|
teams = teams_repo.by_event(tournament_id)
|
||||||
|
|
||||||
|
# Get rounds
|
||||||
|
rounds = rounds_repo.by_event(tournament_id)
|
||||||
|
|
||||||
|
response.render(view,
|
||||||
|
tournament: tournament,
|
||||||
|
players: players,
|
||||||
|
available_players: available_players,
|
||||||
|
teams: teams,
|
||||||
|
rounds: rounds,
|
||||||
|
participants: participants
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
module Teams
|
||||||
|
class Create < EuchreCamp::Action
|
||||||
|
include Deps[
|
||||||
|
teams_repo: 'repositories.teams',
|
||||||
|
players_repo: 'repositories.players',
|
||||||
|
participants_repo: 'repositories.event_participants'
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
params = request.params.to_h
|
||||||
|
|
||||||
|
player_1_id = params[:player_1_id]&.to_i
|
||||||
|
player_2_id = params[:player_2_id]&.to_i
|
||||||
|
team_name = params[:team_name]
|
||||||
|
|
||||||
|
if player_1_id.nil? || player_2_id.nil?
|
||||||
|
response.flash[:error] = "Please select both players"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if player_1_id == player_2_id
|
||||||
|
response.flash[:error] = "Players must be different"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ensure players are registered as participants
|
||||||
|
[player_1_id, player_2_id].each do |player_id|
|
||||||
|
existing = participants_repo.by_event(tournament_id).find { |p| p[:player_id] == player_id }
|
||||||
|
unless existing
|
||||||
|
participants_repo.register_player(tournament_id, player_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create the team
|
||||||
|
team = teams_repo.create(
|
||||||
|
event_id: tournament_id,
|
||||||
|
player_1_id: player_1_id,
|
||||||
|
player_2_id: player_2_id,
|
||||||
|
team_name: team_name || "Team #{player_1_id}-#{player_2_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
response.flash[:message] = "Team created successfully"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to create team: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
module Teams
|
||||||
|
class Destroy < EuchreCamp::Action
|
||||||
|
include Deps[teams_repo: 'repositories.teams']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
team_id = request.params[:team_id]
|
||||||
|
|
||||||
|
teams_repo.delete(team_id.to_i)
|
||||||
|
|
||||||
|
response.flash[:message] = "Team removed"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to remove team: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
module Teams
|
||||||
|
class Edit < EuchreCamp::Action
|
||||||
|
include Deps[
|
||||||
|
teams_repo: 'repositories.teams',
|
||||||
|
players_repo: 'repositories.players',
|
||||||
|
events_repo: 'repositories.events'
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
team_id = request.params[:team_id]
|
||||||
|
|
||||||
|
tournament = events_repo.by_id(tournament_id)
|
||||||
|
team = teams_repo.teams.by_pk(team_id).one
|
||||||
|
players = players_repo.all
|
||||||
|
|
||||||
|
response.render(view,
|
||||||
|
tournament: tournament,
|
||||||
|
team: team,
|
||||||
|
players: players
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
module Teams
|
||||||
|
class Update < EuchreCamp::Action
|
||||||
|
include Deps[teams_repo: 'repositories.teams']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
team_id = request.params[:team_id]
|
||||||
|
params = request.params.to_h
|
||||||
|
|
||||||
|
update_params = {
|
||||||
|
team_name: params[:team_name],
|
||||||
|
player_1_id: params[:player_1_id]&.to_i,
|
||||||
|
player_2_id: params[:player_2_id]&.to_i
|
||||||
|
}
|
||||||
|
|
||||||
|
teams_repo.update(team_id.to_i, update_params)
|
||||||
|
|
||||||
|
response.flash[:message] = "Team updated"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to update team: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}/teams/#{team_id}/edit")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Update < EuchreCamp::Action
|
||||||
|
include Deps[repo: 'repositories.events']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
tournament_id = request.params[:id]
|
||||||
|
params = request.params.to_h
|
||||||
|
|
||||||
|
update_params = {
|
||||||
|
name: params[:name],
|
||||||
|
description: params[:description],
|
||||||
|
event_date: params[:event_date] ? Time.parse(params[:event_date]) : nil,
|
||||||
|
format: params[:format],
|
||||||
|
max_participants: params[:max_participants]&.to_i
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.update(tournament_id, update_params)
|
||||||
|
|
||||||
|
response.flash[:message] = "Tournament updated"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}")
|
||||||
|
rescue StandardError => e
|
||||||
|
response.flash[:error] = "Failed to update: #{e.message}"
|
||||||
|
response.redirect_to("/admin/tournaments/#{tournament_id}/edit")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rom-repository'
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Repositories
|
||||||
|
class BracketMatchups < ::EuchreCamp::Repository[:bracket_matchups]
|
||||||
|
commands :create, update: :by_pk, delete: :by_pk
|
||||||
|
|
||||||
|
def by_event(event_id)
|
||||||
|
bracket_matchups.where(event_id: event_id).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_round(round_id)
|
||||||
|
bracket_matchups.where(round_id: round_id).order(:bracket_position).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_event_and_round(event_id, round_id)
|
||||||
|
bracket_matchups
|
||||||
|
.where(event_id: event_id, round_id: round_id)
|
||||||
|
.order(:bracket_position)
|
||||||
|
.to_a
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rom-repository'
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Repositories
|
||||||
|
class EventParticipants < ::EuchreCamp::Repository[:event_participants]
|
||||||
|
commands :create, update: :by_pk, delete: :by_pk
|
||||||
|
|
||||||
|
def by_event(event_id)
|
||||||
|
event_participants.where(event_id: event_id).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def players_by_event(event_id)
|
||||||
|
# Return player IDs registered for an event
|
||||||
|
event_participants
|
||||||
|
.where(event_id: event_id)
|
||||||
|
.select(:player_id)
|
||||||
|
.to_a
|
||||||
|
.map { |p| p[:player_id] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def register_player(event_id, player_id, seed: nil)
|
||||||
|
create(
|
||||||
|
event_id: event_id,
|
||||||
|
player_id: player_id,
|
||||||
|
status: 'registered',
|
||||||
|
seed: seed
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_player(event_id, player_id)
|
||||||
|
event_participants
|
||||||
|
.where(event_id: event_id, player_id: player_id)
|
||||||
|
.delete
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rom-repository'
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Repositories
|
||||||
|
class Events < ::EuchreCamp::Repository[:events]
|
||||||
|
commands :create, update: :by_pk, delete: :by_pk
|
||||||
|
|
||||||
|
def all
|
||||||
|
events.to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_id(id)
|
||||||
|
events.by_pk(id).one!
|
||||||
|
end
|
||||||
|
|
||||||
|
def upcoming
|
||||||
|
events.where(status: ['planned', 'active']).order(:event_date).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def completed
|
||||||
|
events.where(status: 'completed').order(Sequel.desc(:event_date)).to_a
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rom-repository'
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Repositories
|
||||||
|
class PartnershipGames < ::EuchreCamp::Repository[:partnership_games]
|
||||||
|
commands :create, update: :by_pk, delete: :by_pk
|
||||||
|
|
||||||
|
def by_player(player_id)
|
||||||
|
partnership_games.where(
|
||||||
|
Sequel.|({ player_1_id: player_id }, { player_2_id: player_id })
|
||||||
|
).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_match(match_id)
|
||||||
|
partnership_games.where(match_id: match_id).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_partnership(player_1_id, player_2_id)
|
||||||
|
partnership_games.where(
|
||||||
|
Sequel.|(
|
||||||
|
{ player_1_id: player_1_id, player_2_id: player_2_id },
|
||||||
|
{ player_1_id: player_2_id, player_2_id: player_1_id }
|
||||||
|
)
|
||||||
|
).to_a
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rom-repository'
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Repositories
|
||||||
|
class PartnershipStats < ::EuchreCamp::Repository[:partnership_stats]
|
||||||
|
commands :create, update: :by_pk
|
||||||
|
|
||||||
|
def find_or_create(player_1_id, player_2_id)
|
||||||
|
# Ensure consistent ordering (lower ID first)
|
||||||
|
p1_id = [player_1_id, player_2_id].min
|
||||||
|
p2_id = [player_1_id, player_2_id].max
|
||||||
|
|
||||||
|
stats = partnership_stats.where(player_1_id: p1_id, player_2_id: p2_id).one
|
||||||
|
|
||||||
|
return stats if stats
|
||||||
|
|
||||||
|
create(
|
||||||
|
player_1_id: p1_id,
|
||||||
|
player_2_id: p2_id,
|
||||||
|
games_played: 0,
|
||||||
|
wins: 0,
|
||||||
|
losses: 0,
|
||||||
|
total_elo_change: 0
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_player(player_id)
|
||||||
|
partnership_stats.where(
|
||||||
|
Sequel.|({ player_1_id: player_id }, { player_2_id: player_id })
|
||||||
|
).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_stats(player_1_id, player_2_id, won: false, elo_change: 0)
|
||||||
|
stats = find_or_create(player_1_id, player_2_id)
|
||||||
|
|
||||||
|
update_params = {
|
||||||
|
games_played: stats.games_played + 1,
|
||||||
|
wins: stats.wins + (won ? 1 : 0),
|
||||||
|
losses: stats.losses + (won ? 0 : 1),
|
||||||
|
total_elo_change: stats.total_elo_change + elo_change,
|
||||||
|
last_played: Time.now,
|
||||||
|
updated_at: Time.now
|
||||||
|
}
|
||||||
|
|
||||||
|
update(stats.id, update_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rom-repository'
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Repositories
|
||||||
|
class Teams < ::EuchreCamp::Repository[:teams]
|
||||||
|
commands :create, update: :by_pk, delete: :by_pk
|
||||||
|
|
||||||
|
def by_event(event_id)
|
||||||
|
teams.where(event_id: event_id).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create(event_id, player_1_id, player_2_id, team_name = nil)
|
||||||
|
existing = teams.where(
|
||||||
|
event_id: event_id,
|
||||||
|
player_1_id: [player_1_id, player_2_id],
|
||||||
|
player_2_id: [player_1_id, player_2_id]
|
||||||
|
).one
|
||||||
|
|
||||||
|
return existing if existing
|
||||||
|
|
||||||
|
create(
|
||||||
|
event_id: event_id,
|
||||||
|
player_1_id: player_1_id,
|
||||||
|
player_2_id: player_2_id,
|
||||||
|
team_name: team_name || "Team #{player_1_id}-#{player_2_id}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rom-repository'
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Repositories
|
||||||
|
class TournamentRounds < ::EuchreCamp::Repository[:tournament_rounds]
|
||||||
|
commands :create, update: :by_pk, delete: :by_pk
|
||||||
|
|
||||||
|
def by_event(event_id)
|
||||||
|
tournament_rounds.where(event_id: event_id).order(:round_number).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_round(event_id)
|
||||||
|
tournament_rounds
|
||||||
|
.where(event_id: event_id, status: 'active')
|
||||||
|
.order(Sequel.desc(:round_number))
|
||||||
|
.one
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_round(event_id)
|
||||||
|
tournament_rounds
|
||||||
|
.where(event_id: event_id, status: 'pending')
|
||||||
|
.order(:round_number)
|
||||||
|
.limit(1)
|
||||||
|
.one
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Services
|
||||||
|
class PartnershipTracker
|
||||||
|
# Record partnerships when a match completes
|
||||||
|
def self.record_match(match_id)
|
||||||
|
rom = EuchreCamp::App["persistence.rom"]
|
||||||
|
match = rom.relations[:matches].by_pk(match_id).one
|
||||||
|
|
||||||
|
return unless match
|
||||||
|
|
||||||
|
games_repo = EuchreCamp::App["repositories.partnership_games"]
|
||||||
|
stats_repo = EuchreCamp::App["repositories.partnership_stats"]
|
||||||
|
|
||||||
|
# Calculate Elo changes for each player
|
||||||
|
# This is called AFTER Elo calculation, so we get the change from elo_snapshots
|
||||||
|
elo_changes = calculate_elo_changes(match_id)
|
||||||
|
|
||||||
|
# Record Team 1 partnership
|
||||||
|
record_partnership(
|
||||||
|
games_repo: games_repo,
|
||||||
|
stats_repo: stats_repo,
|
||||||
|
match_id: match_id,
|
||||||
|
player_1_id: match[:team_1_p1_id],
|
||||||
|
player_2_id: match[:team_1_p2_id],
|
||||||
|
team_number: 1,
|
||||||
|
won: match[:team_1_score] > match[:team_2_score],
|
||||||
|
elo_change: elo_changes[match[:team_1_p1_id]] + elo_changes[match[:team_1_p2_id]]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Record Team 2 partnership
|
||||||
|
record_partnership(
|
||||||
|
games_repo: games_repo,
|
||||||
|
stats_repo: stats_repo,
|
||||||
|
match_id: match_id,
|
||||||
|
player_1_id: match[:team_2_p1_id],
|
||||||
|
player_2_id: match[:team_2_p2_id],
|
||||||
|
team_number: 2,
|
||||||
|
won: match[:team_2_score] > match[:team_1_score],
|
||||||
|
elo_change: elo_changes[match[:team_2_p1_id]] + elo_changes[match[:team_2_p2_id]]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.calculate_elo_changes(match_id)
|
||||||
|
rom = EuchreCamp::App["persistence.rom"]
|
||||||
|
|
||||||
|
# Get the two most recent snapshots before and after this match
|
||||||
|
snapshots = rom.relations[:elo_snapshots]
|
||||||
|
.where(match_id: match_id)
|
||||||
|
.order(:created_at)
|
||||||
|
.to_a
|
||||||
|
|
||||||
|
# Group by player and calculate change
|
||||||
|
changes = Hash.new(0)
|
||||||
|
snapshots.group_by { |s| s[:player_id] }.each do |player_id, player_snapshots|
|
||||||
|
# For a single match, there should be exactly one snapshot per player
|
||||||
|
# The snapshot contains rating_before and rating_after for this specific match
|
||||||
|
# So we calculate the change directly from that snapshot
|
||||||
|
player_snapshots.each do |snapshot|
|
||||||
|
changes[player_id] = snapshot[:rating_after] - snapshot[:rating_before]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
changes
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.record_partnership(games_repo:, stats_repo:, match_id:, player_1_id:, player_2_id:, team_number:, won:, elo_change:)
|
||||||
|
# Record the partnership game
|
||||||
|
games_repo.create(
|
||||||
|
match_id: match_id,
|
||||||
|
player_1_id: player_1_id,
|
||||||
|
player_2_id: player_2_id,
|
||||||
|
team_number: team_number,
|
||||||
|
won_match: won ? 1 : 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update partnership statistics (using consistent ordering)
|
||||||
|
stats_repo.update_stats(player_1_id, player_2_id, won: won, elo_change: elo_change)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get partnership statistics for a player
|
||||||
|
def self.get_player_partnerships(player_id)
|
||||||
|
stats_repo = EuchreCamp::App["repositories.partnership_stats"]
|
||||||
|
players_repo = EuchreCamp::App["repositories.players"]
|
||||||
|
|
||||||
|
stats = stats_repo.by_player(player_id)
|
||||||
|
all_players = players_repo.all.to_h { |p| [p[:id], p] }
|
||||||
|
|
||||||
|
stats.map do |stat|
|
||||||
|
partner_id = stat[:player_1_id] == player_id ? stat[:player_2_id] : stat[:player_1_id]
|
||||||
|
partner = all_players[partner_id]
|
||||||
|
|
||||||
|
{
|
||||||
|
partner: partner,
|
||||||
|
games_played: stat[:games_played],
|
||||||
|
wins: stat[:wins],
|
||||||
|
losses: stat[:losses],
|
||||||
|
win_rate: stat[:games_played] > 0 ? (stat[:wins].to_f / stat[:games_played]).round(3) : 0,
|
||||||
|
total_elo_change: stat[:total_elo_change],
|
||||||
|
avg_elo_change: stat[:games_played] > 0 ? (stat[:total_elo_change].to_f / stat[:games_played]).round(2) : 0,
|
||||||
|
last_played: stat[:last_played]
|
||||||
|
}
|
||||||
|
end.sort_by { |p| -p[:games_played] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Services
|
||||||
|
class TournamentGenerator
|
||||||
|
# Generate matchups for round-robin tournament
|
||||||
|
# Uses circle method for scheduling
|
||||||
|
def self.round_robin(teams)
|
||||||
|
n = teams.length
|
||||||
|
return [] if n < 2
|
||||||
|
|
||||||
|
# Handle odd number of teams by adding a bye
|
||||||
|
teams_with_bye = teams.dup
|
||||||
|
bye_team = { id: nil, name: "BYE" }
|
||||||
|
teams_with_bye << bye_team if n.odd?
|
||||||
|
|
||||||
|
total_teams = teams_with_bye.length
|
||||||
|
rounds = []
|
||||||
|
|
||||||
|
# Circle method: fix first team, rotate others
|
||||||
|
(total_teams - 1).times do |round_num|
|
||||||
|
round_matchups = []
|
||||||
|
half = total_teams / 2
|
||||||
|
|
||||||
|
half.times do |i|
|
||||||
|
team_a = teams_with_bye[i]
|
||||||
|
team_b = teams_with_bye[total_teams - 1 - i]
|
||||||
|
|
||||||
|
# Skip if either is BYE
|
||||||
|
next if team_a[:id].nil? || team_b[:id].nil?
|
||||||
|
|
||||||
|
round_matchups << {
|
||||||
|
team_1_id: team_a[:id],
|
||||||
|
team_2_id: team_b[:id]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
rounds << {
|
||||||
|
round_number: round_num + 1,
|
||||||
|
matchups: round_matchups
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rotate teams (circle method)
|
||||||
|
teams_with_bye = [teams_with_bye[0]] + teams_with_bye[1..-1].rotate(-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
rounds
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate single elimination bracket
|
||||||
|
def self.single_elimination(seeded_teams)
|
||||||
|
# seeded_teams should be array of { id: team_id, seed: number }
|
||||||
|
teams = seeded_teams.sort_by { |t| t[:seed] || 0 }
|
||||||
|
|
||||||
|
# Ensure power of 2 or add byes
|
||||||
|
bracket_size = 1
|
||||||
|
while bracket_size < teams.length
|
||||||
|
bracket_size *= 2
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create bracket structure
|
||||||
|
bracket = []
|
||||||
|
positions = {}
|
||||||
|
|
||||||
|
# Calculate bracket positions
|
||||||
|
(1..bracket_size).each do |pos|
|
||||||
|
positions[pos] = pos
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fill bracket with teams and byes
|
||||||
|
team_index = 0
|
||||||
|
(1..bracket_size).each do |pos|
|
||||||
|
if team_index < teams.length
|
||||||
|
positions[pos] = teams[team_index][:id]
|
||||||
|
team_index += 1
|
||||||
|
else
|
||||||
|
positions[pos] = nil # BYE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate matchups for first round
|
||||||
|
matchups = []
|
||||||
|
num_matchups = bracket_size / 2
|
||||||
|
num_matchups.times do |i|
|
||||||
|
pos1 = i * 2 + 1
|
||||||
|
pos2 = i * 2 + 2
|
||||||
|
team1 = positions[pos1]
|
||||||
|
team2 = positions[pos2]
|
||||||
|
|
||||||
|
# Only add matchup if both teams exist (no BYE vs BYE)
|
||||||
|
unless team1.nil? && team2.nil?
|
||||||
|
matchups << {
|
||||||
|
team_1_id: team1,
|
||||||
|
team_2_id: team2,
|
||||||
|
bracket_position: i + 1,
|
||||||
|
winner_position: (i / 2) + 1 # Where winner advances to
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
bracket_size: bracket_size,
|
||||||
|
first_round_matchups: matchups,
|
||||||
|
bracket_positions: positions
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate double elimination bracket
|
||||||
|
def self.double_elimination(seeded_teams)
|
||||||
|
# For simplicity, returning structure for winner's bracket and loser's bracket
|
||||||
|
teams = seeded_teams.sort_by { |t| t[:seed] || 0 }
|
||||||
|
|
||||||
|
# Generate winner's bracket (same as single elim)
|
||||||
|
winners_bracket = single_elimination(teams)
|
||||||
|
|
||||||
|
# Loser's bracket structure (simplified)
|
||||||
|
# In full implementation, this would be more complex
|
||||||
|
loser_bracket_rounds = []
|
||||||
|
|
||||||
|
{
|
||||||
|
winners_bracket: winners_bracket,
|
||||||
|
loser_bracket: loser_bracket_rounds
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate Swiss system pairings
|
||||||
|
def self.swiss(teams, round_number, standings = {})
|
||||||
|
# standings should be hash of team_id => { wins, losses, points }
|
||||||
|
return [] if teams.empty?
|
||||||
|
|
||||||
|
# Sort teams by current score (descending)
|
||||||
|
sorted_teams = teams.sort_by do |team|
|
||||||
|
score = standings[team[:id]] || { wins: 0, losses: 0, points: 0 }
|
||||||
|
[-score[:wins], -score[:points]]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pair similar scores
|
||||||
|
matchups = []
|
||||||
|
paired = Set.new
|
||||||
|
|
||||||
|
sorted_teams.each_with_index do |team, i|
|
||||||
|
next if paired.include?(team[:id])
|
||||||
|
|
||||||
|
# Find opponent with similar score
|
||||||
|
opponent = nil
|
||||||
|
(i + 1...sorted_teams.length).each do |j|
|
||||||
|
candidate = sorted_teams[j]
|
||||||
|
unless paired.include?(candidate[:id])
|
||||||
|
opponent = candidate
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if opponent
|
||||||
|
matchups << {
|
||||||
|
team_1_id: team[:id],
|
||||||
|
team_2_id: opponent[:id]
|
||||||
|
}
|
||||||
|
paired << team[:id]
|
||||||
|
paired << opponent[:id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[{
|
||||||
|
round_number: round_number,
|
||||||
|
matchups: matchups
|
||||||
|
}]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculate number of matches for each format
|
||||||
|
def self.match_count(format, participant_count)
|
||||||
|
case format
|
||||||
|
when 'round_robin'
|
||||||
|
participant_count * (participant_count - 1) / 2
|
||||||
|
when 'single_elim'
|
||||||
|
participant_count - 1
|
||||||
|
when 'double_elim'
|
||||||
|
(participant_count * 2) - 2
|
||||||
|
when 'swiss'
|
||||||
|
# Assuming log2(participant_count) rounds
|
||||||
|
rounds = Math.log2(participant_count).ceil
|
||||||
|
(participant_count * rounds) / 2
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate participant count for format
|
||||||
|
def self.valid_participant_count?(format, count)
|
||||||
|
case format
|
||||||
|
when 'round_robin'
|
||||||
|
count >= 2
|
||||||
|
when 'single_elim', 'double_elim'
|
||||||
|
# Must be power of 2 for pure bracket, or allow byes
|
||||||
|
count >= 2
|
||||||
|
when 'swiss'
|
||||||
|
count >= 2
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<h1>Edit Tournament: <%= tournament.name %></h1>
|
||||||
|
|
||||||
|
<form action="/admin/tournaments/<%= tournament.id %>" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||||
|
<input type="hidden" name="_method" value="patch">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="name">Tournament Name *</label>
|
||||||
|
<input type="text" id="name" name="name" value="<%= tournament.name %>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description" rows="3"><%= tournament.description %></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="event_date">Date/Time</label>
|
||||||
|
<input type="datetime-local" id="event_date" name="event_date"
|
||||||
|
value="<%= tournament.event_date&.strftime('%Y-%m-%dT%H:%M') %>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="format">Tournament Format</label>
|
||||||
|
<select id="format" name="format">
|
||||||
|
<option value="single_elim" <%= 'selected' if tournament.format == 'single_elim' %>>Single Elimination</option>
|
||||||
|
<option value="double_elim" <%= 'selected' if tournament.format == 'double_elim' %>>Double Elimination</option>
|
||||||
|
<option value="round_robin" <%= 'selected' if tournament.format == 'round_robin' %>>Round Robin</option>
|
||||||
|
<option value="swiss" <%= 'selected' if tournament.format == 'swiss' %>>Swiss System</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="max_participants">Max Participants (teams)</label>
|
||||||
|
<input type="number" id="max_participants" name="max_participants"
|
||||||
|
min="2" max="32" value="<%= tournament.max_participants %>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Update Tournament</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p><a href="/admin/tournaments/<%= tournament.id %>">Cancel</a></p>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<h1>Tournament Administration</h1>
|
||||||
|
|
||||||
|
<p><a href="/admin/tournaments/new">Create New Tournament</a></p>
|
||||||
|
|
||||||
|
<% if upcoming.any? %>
|
||||||
|
<h2>Upcoming / Active Tournaments</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Format</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% upcoming.each do |tournament| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= tournament.name %></td>
|
||||||
|
<td><%= tournament.event_date&.strftime('%Y-%m-%d %H:%M') %></td>
|
||||||
|
<td><%= tournament.format %></td>
|
||||||
|
<td><%= tournament.status %></td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/tournaments/<%= tournament.id %>">View</a> |
|
||||||
|
<a href="/admin/tournaments/<%= tournament.id %>/edit">Edit</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if completed.any? %>
|
||||||
|
<h2>Past Tournaments</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Format</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% completed.each do |tournament| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= tournament.name %></td>
|
||||||
|
<td><%= tournament.event_date&.strftime('%Y-%m-%d') %></td>
|
||||||
|
<td><%= tournament.format %></td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/tournaments/<%= tournament.id %>">View Results</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if upcoming.empty? && completed.empty? %>
|
||||||
|
<p>No tournaments yet. <a href="/admin/tournaments/new">Create your first tournament!</a></p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p><a href="/admin/players">Back to Admin</a></p>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<h1>Create New Tournament</h1>
|
||||||
|
|
||||||
|
<form action="/admin/tournaments" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="name">Tournament Name *</label>
|
||||||
|
<input type="text" id="name" name="name" required placeholder="e.g., Spring Championship">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description" rows="3" placeholder="Optional description or rules..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="event_date">Date/Time</label>
|
||||||
|
<input type="datetime-local" id="event_date" name="event_date" value="<%= Time.now.strftime('%Y-%m-%dT%H:%M') %>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="format">Tournament Format</label>
|
||||||
|
<select id="format" name="format">
|
||||||
|
<option value="single_elim">Single Elimination</option>
|
||||||
|
<option value="double_elim">Double Elimination</option>
|
||||||
|
<option value="round_robin">Round Robin</option>
|
||||||
|
<option value="swiss">Swiss System</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="max_participants">Max Participants (teams)</label>
|
||||||
|
<input type="number" id="max_participants" name="max_participants" min="2" max="32" placeholder="Optional">
|
||||||
|
<small>Leave blank for no limit</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Create Tournament</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p><a href="/admin/tournaments">Cancel</a></p>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<h1>Enter Results: <%= tournament.name %> - Round <%= round[:round_number] %></h1>
|
||||||
|
|
||||||
|
<form action="/admin/tournaments/<%= tournament.id %>/results" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Matchup</th>
|
||||||
|
<th>Team 1</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>vs</th>
|
||||||
|
<th>Team 2</th>
|
||||||
|
<th>Score</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% matchups.each_with_index do |matchup, i| %>
|
||||||
|
<% team_1 = teams.find { |t| t.id == matchup[:team_1_id] } %>
|
||||||
|
<% team_2 = teams.find { |t| t.id == matchup[:team_2_id] } %>
|
||||||
|
<% next if team_1.nil? || team_2.nil? %>
|
||||||
|
<tr>
|
||||||
|
<td><%= i + 1 %></td>
|
||||||
|
<td><%= team_1.team_name %></td>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="team_1_score_<%= matchup[:id] %>" min="0" max="10" value="10" style="width: 60px;">
|
||||||
|
</td>
|
||||||
|
<td>vs</td>
|
||||||
|
<td><%= team_2.team_name %></td>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="team_2_score_<%= matchup[:id] %>" min="0" max="10" value="7" style="width: 60px;">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="matchup_<%= matchup[:id] %>" checked style="display:none;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<button type="submit">Save All Results</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p><a href="/admin/tournaments/<%= tournament.id %>">Back to Tournament</a></p>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<h1><%= tournament.name %> - Round <%= round[:round_number] %></h1>
|
||||||
|
|
||||||
|
<p><strong>Status:</strong> <%= round[:status] %></p>
|
||||||
|
|
||||||
|
<h2>Matchups</h2>
|
||||||
|
|
||||||
|
<% if matchups.any? %>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Position</th>
|
||||||
|
<th>Team 1</th>
|
||||||
|
<th>vs</th>
|
||||||
|
<th>Team 2</th>
|
||||||
|
<th>Winner</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% matchups.each do |matchup| %>
|
||||||
|
<% team_1 = teams.find { |t| t.id == matchup[:team_1_id] } %>
|
||||||
|
<% team_2 = teams.find { |t| t.id == matchup[:team_2_id] } %>
|
||||||
|
<% winner = teams.find { |t| t.id == matchup[:winner_team_id] } %>
|
||||||
|
<tr>
|
||||||
|
<td><%= matchup[:bracket_position] %></td>
|
||||||
|
<td><%= team_1&.team_name || 'BYE' %></td>
|
||||||
|
<td>vs</td>
|
||||||
|
<td><%= team_2&.team_name || 'BYE' %></td>
|
||||||
|
<td><%= winner&.team_name || '-' %></td>
|
||||||
|
<td>
|
||||||
|
<% if matchup[:match_id].nil? %>
|
||||||
|
<a href="/admin/matches/new?event_id=<%= tournament.id %>&team_1_id=<%= matchup[:team_1_id] %>&team_2_id=<%= matchup[:team_2_id] %>">
|
||||||
|
Create Match
|
||||||
|
</a>
|
||||||
|
<% else %>
|
||||||
|
<a href="/admin/matches/<%= matchup[:match_id] %>">View Match</a>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% else %>
|
||||||
|
<p>No matchups scheduled for this round.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p><a href="/admin/tournaments/<%= tournament.id %>">Back to Tournament</a></p>
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
<h1><%= tournament.name %></h1>
|
||||||
|
|
||||||
|
<% if tournament.description %>
|
||||||
|
<p><em><%= tournament.description %></em></p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="tournament-meta">
|
||||||
|
<strong>Format:</strong> <%= tournament.format %> |
|
||||||
|
<strong>Status:</strong> <%= tournament.status %> |
|
||||||
|
<% if tournament.event_date %>
|
||||||
|
<strong>Date:</strong> <%= tournament.event_date.strftime('%Y-%m-%d %H:%M') %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="tournament-tabs">
|
||||||
|
<ul>
|
||||||
|
<li><a href="#participants">Participants</a></li>
|
||||||
|
<li><a href="#teams">Teams</a></li>
|
||||||
|
<li><a href="#rounds">Rounds & Schedule</a></li>
|
||||||
|
<li><a href="#standings">Standings</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Participants Section -->
|
||||||
|
<section id="participants">
|
||||||
|
<h2>Participants</h2>
|
||||||
|
|
||||||
|
<% if players.any? %>
|
||||||
|
<p><strong>Total:</strong> <%= players.count %> players registered</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Player</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Seed</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% players.each do |player| %>
|
||||||
|
<% participant = participants.find { |p| p.player_id == player.id } %>
|
||||||
|
<tr>
|
||||||
|
<td><%= player.name %></td>
|
||||||
|
<td><%= participant&.status || 'registered' %></td>
|
||||||
|
<td><%= participant&.seed || '-' %></td>
|
||||||
|
<td>
|
||||||
|
<form action="/admin/tournaments/<%= tournament.id %>/participants/<%= player.id %>" method="post" style="display:inline;">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||||
|
<input type="hidden" name="_method" value="delete">
|
||||||
|
<button type="submit" onclick="return confirm('Remove <%= player.name %> from tournament?')" style="background:none;border:none;color:blue;text-decoration:underline;cursor:pointer;">Remove</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% else %>
|
||||||
|
<p>No participants registered yet.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<h3>Add Participants</h3>
|
||||||
|
<% if available_players.any? %>
|
||||||
|
<form action="/admin/tournaments/<%= tournament.id %>/participants" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||||
|
<label>Add Player:</label>
|
||||||
|
<select name="player_id">
|
||||||
|
<option value="">Select a player...</option>
|
||||||
|
<% available_players.each do |player| %>
|
||||||
|
<option value="<%= player.id %>"><%= player.name %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
<button type="submit">Add to Tournament</button>
|
||||||
|
</form>
|
||||||
|
<% else %>
|
||||||
|
<p><em>All available players are already registered.</em></p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Teams Section -->
|
||||||
|
<section id="teams">
|
||||||
|
<h2>Teams</h2>
|
||||||
|
|
||||||
|
<% if teams.any? %>
|
||||||
|
<p><strong>Total:</strong> <%= teams.count %> teams</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Team Name</th>
|
||||||
|
<th>Player 1</th>
|
||||||
|
<th>Player 2</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% teams.each do |team| %>
|
||||||
|
<% p1 = players.find { |p| p.id == team.player_1_id } %>
|
||||||
|
<% p2 = players.find { |p| p.id == team.player_2_id } %>
|
||||||
|
<tr>
|
||||||
|
<td><%= team.team_name %></td>
|
||||||
|
<td><%= p1&.name || team.player_1_id %></td>
|
||||||
|
<td><%= p2&.name || team.player_2_id %></td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/tournaments/<%= tournament.id %>/teams/<%= team.id %>/edit">Edit</a> |
|
||||||
|
<form action="/admin/tournaments/<%= tournament.id %>/teams/<%= team.id %>" method="post" style="display:inline;">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||||
|
<input type="hidden" name="_method" value="delete">
|
||||||
|
<button type="submit" onclick="return confirm('Remove team?')" style="background:none;border:none;color:blue;text-decoration:underline;cursor:pointer;">Remove</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% else %>
|
||||||
|
<p>No teams created yet.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<h3>Create Teams</h3>
|
||||||
|
<% if players.any? %>
|
||||||
|
<form action="/admin/tournaments/<%= tournament.id %>/teams" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||||
|
<div>
|
||||||
|
<label>Team Name:</label>
|
||||||
|
<input type="text" name="team_name" placeholder="e.g., Ace High">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Player 1:</label>
|
||||||
|
<select name="player_1_id">
|
||||||
|
<option value="">Select player...</option>
|
||||||
|
<% players.each do |player| %>
|
||||||
|
<option value="<%= player.id %>"><%= player.name %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Player 2:</label>
|
||||||
|
<select name="player_2_id">
|
||||||
|
<option value="">Select player...</option>
|
||||||
|
<% players.each do |player| %>
|
||||||
|
<option value="<%= player.id %>"><%= player.name %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Create Team</button>
|
||||||
|
</form>
|
||||||
|
<% else %>
|
||||||
|
<p><em>Add participants first before creating teams.</em></p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Rounds Section -->
|
||||||
|
<section id="rounds">
|
||||||
|
<h2>Rounds & Schedule</h2>
|
||||||
|
|
||||||
|
<% if rounds.any? %>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Round</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% rounds.each do |round| %>
|
||||||
|
<tr>
|
||||||
|
<td>Round <%= round.round_number %></td>
|
||||||
|
<td><%= round.status %></td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/tournaments/<%= tournament.id %>/rounds/<%= round.id %>">View</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% else %>
|
||||||
|
<p>No rounds scheduled yet.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if teams.any? %>
|
||||||
|
<form action="/admin/tournaments/<%= tournament.id %>/schedule" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||||
|
<button type="submit">Generate Schedule / Rounds</button>
|
||||||
|
</form>
|
||||||
|
<% else %>
|
||||||
|
<p><em>Add teams first to generate the schedule.</em></p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Standings Section -->
|
||||||
|
<section id="standings">
|
||||||
|
<h2>Standings</h2>
|
||||||
|
|
||||||
|
<% if teams.any? %>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Team</th>
|
||||||
|
<th>W</th>
|
||||||
|
<th>L</th>
|
||||||
|
<th>PCT</th>
|
||||||
|
<th>PF</th>
|
||||||
|
<th>PA</th>
|
||||||
|
<th>PD</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% teams.each_with_index do |team, i| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= i + 1 %></td>
|
||||||
|
<td><%= team.team_name %></td>
|
||||||
|
<td>0</td>
|
||||||
|
<td>0</td>
|
||||||
|
<td>.000</td>
|
||||||
|
<td>0</td>
|
||||||
|
<td>0</td>
|
||||||
|
<td>0</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% else %>
|
||||||
|
<p>No teams to display standings.</p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="tournament-actions">
|
||||||
|
<a href="/admin/tournaments/<%= tournament.id %>/results">Enter Match Results</a> |
|
||||||
|
<a href="/admin/tournaments/<%= tournament.id %>/complete">Complete Tournament</a> |
|
||||||
|
<a href="/admin/tournaments">Back to Tournaments</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<h1>Edit Team: <%= team.team_name %></h1>
|
||||||
|
|
||||||
|
<p><strong>Tournament:</strong> <%= tournament.name %></p>
|
||||||
|
|
||||||
|
<form action="/admin/tournaments/<%= tournament.id %>/teams/<%= team.id %>" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||||
|
<input type="hidden" name="_method" value="patch">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="team_name">Team Name</label>
|
||||||
|
<input type="text" id="team_name" name="team_name" value="<%= team.team_name %>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="player_1_id">Player 1</label>
|
||||||
|
<select id="player_1_id" name="player_1_id" required>
|
||||||
|
<% players.each do |player| %>
|
||||||
|
<option value="<%= player.id %>" <%= 'selected' if player.id == team.player_1_id %>>
|
||||||
|
<%= player.name %>
|
||||||
|
</option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="player_2_id">Player 2</label>
|
||||||
|
<select id="player_2_id" name="player_2_id" required>
|
||||||
|
<% players.each do |player| %>
|
||||||
|
<option value="<%= player.id %>" <%= 'selected' if player.id == team.player_2_id %>>
|
||||||
|
<%= player.name %>
|
||||||
|
</option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Update Team</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p><a href="/admin/tournaments/<%= tournament.id %>">Back to Tournament</a></p>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Views
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Edit < EuchreCamp::View
|
||||||
|
expose :tournament
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Views
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Index < EuchreCamp::View
|
||||||
|
expose :upcoming
|
||||||
|
expose :completed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Views
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class New < EuchreCamp::View
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Views
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Results < EuchreCamp::View
|
||||||
|
expose :tournament
|
||||||
|
expose :round
|
||||||
|
expose :matchups
|
||||||
|
expose :teams
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Views
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
module Rounds
|
||||||
|
class Show < EuchreCamp::View
|
||||||
|
expose :tournament
|
||||||
|
expose :round
|
||||||
|
expose :matchups
|
||||||
|
expose :teams
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Views
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
class Show < EuchreCamp::View
|
||||||
|
expose :tournament
|
||||||
|
expose :players
|
||||||
|
expose :available_players
|
||||||
|
expose :teams
|
||||||
|
expose :rounds
|
||||||
|
expose :participants
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Views
|
||||||
|
module Admin
|
||||||
|
module Tournaments
|
||||||
|
module Teams
|
||||||
|
class Edit < EuchreCamp::View
|
||||||
|
expose :tournament
|
||||||
|
expose :team
|
||||||
|
expose :players
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
ROM::SQL.migration do
|
||||||
|
change do
|
||||||
|
# Add new columns to events table
|
||||||
|
alter_table :events do
|
||||||
|
add_column :description, String
|
||||||
|
add_column :event_date, DateTime
|
||||||
|
add_column :event_type, String, default: 'tournament'
|
||||||
|
add_column :format, String, default: 'single_elim'
|
||||||
|
add_column :status, String, default: 'planned'
|
||||||
|
add_column :max_participants, Integer
|
||||||
|
add_column :created_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
|
||||||
|
add_column :updated_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
|
||||||
|
end
|
||||||
|
|
||||||
|
# Remove the self-referential column that's not being used
|
||||||
|
# (keeping it for potential nested tournaments in future)
|
||||||
|
# alter_table :events do
|
||||||
|
# drop_column :event_id
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
ROM::SQL.migration do
|
||||||
|
change do
|
||||||
|
create_table :teams do
|
||||||
|
primary_key :id
|
||||||
|
foreign_key :event_id, :events, null: false
|
||||||
|
column :team_name, String
|
||||||
|
column :player_1_id, Integer, null: false
|
||||||
|
column :player_2_id, Integer, null: false
|
||||||
|
column :created_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :teams, [:event_id, :player_1_id, :player_2_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
ROM::SQL.migration do
|
||||||
|
change do
|
||||||
|
create_table :event_participants do
|
||||||
|
primary_key :id
|
||||||
|
foreign_key :event_id, :events, null: false
|
||||||
|
foreign_key :player_id, :players, null: false
|
||||||
|
foreign_key :team_id, :teams
|
||||||
|
column :status, String, default: 'registered' # registered, checked_in, active, eliminated
|
||||||
|
column :seed, Integer
|
||||||
|
column :registration_date, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :event_participants, [:event_id, :player_id], unique: true
|
||||||
|
add_index :event_participants, [:event_id, :team_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
ROM::SQL.migration do
|
||||||
|
change do
|
||||||
|
create_table :tournament_rounds do
|
||||||
|
primary_key :id
|
||||||
|
foreign_key :event_id, :events, null: false
|
||||||
|
column :round_number, Integer, null: false
|
||||||
|
column :status, String, default: 'pending' # pending, active, completed
|
||||||
|
column :scheduled_start, DateTime
|
||||||
|
column :actual_start, DateTime
|
||||||
|
column :completed_at, DateTime
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :tournament_rounds, [:event_id, :round_number], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
ROM::SQL.migration do
|
||||||
|
change do
|
||||||
|
create_table :bracket_matchups do
|
||||||
|
primary_key :id
|
||||||
|
foreign_key :event_id, :events, null: false
|
||||||
|
foreign_key :round_id, :tournament_rounds, null: false
|
||||||
|
foreign_key :match_id, :matches
|
||||||
|
foreign_key :team_1_id, :teams
|
||||||
|
foreign_key :team_2_id, :teams
|
||||||
|
column :bracket_position, Integer
|
||||||
|
column :winner_team_id, Integer
|
||||||
|
column :loser_team_id, Integer
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :bracket_matchups, [:event_id, :round_id, :bracket_position], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
ROM::SQL.migration do
|
||||||
|
change do
|
||||||
|
# Track each instance of a partnership playing together in a match
|
||||||
|
create_table :partnership_games do
|
||||||
|
primary_key :id
|
||||||
|
foreign_key :match_id, :matches, null: false
|
||||||
|
foreign_key :player_1_id, :players, null: false
|
||||||
|
foreign_key :player_2_id, :players, null: false
|
||||||
|
column :team_number, Integer, null: false # 1 or 2
|
||||||
|
column :won_match, Integer, null: false # 0 or 1 (SQLite doesn't have boolean)
|
||||||
|
column :created_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :partnership_games, [:player_1_id, :player_2_id, :match_id], unique: true
|
||||||
|
add_index :partnership_games, :match_id
|
||||||
|
add_index :partnership_games, [:player_1_id, :player_2_id]
|
||||||
|
|
||||||
|
# Summary statistics for partnerships (denormalized for performance)
|
||||||
|
create_table :partnership_stats do
|
||||||
|
primary_key :id
|
||||||
|
foreign_key :player_1_id, :players, null: false
|
||||||
|
foreign_key :player_2_id, :players, null: false
|
||||||
|
column :games_played, Integer, default: 0
|
||||||
|
column :wins, Integer, default: 0
|
||||||
|
column :losses, Integer, default: 0
|
||||||
|
column :total_elo_change, Integer, default: 0
|
||||||
|
column :last_played, DateTime
|
||||||
|
column :created_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
|
||||||
|
column :updated_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :partnership_stats, [:player_1_id, :player_2_id], unique: true
|
||||||
|
add_index :partnership_stats, [:player_2_id, :player_1_id] # For reverse lookups
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Persistence
|
||||||
|
module Relations
|
||||||
|
class BracketMatchups < ROM::Relation[:sql]
|
||||||
|
schema(:bracket_matchups, infer: true) do
|
||||||
|
attribute :id, ROM::Types::Integer, primary_key: true
|
||||||
|
attribute :event_id, ROM::Types::Integer
|
||||||
|
attribute :round_id, ROM::Types::Integer
|
||||||
|
attribute :match_id, ROM::Types::Integer.optional
|
||||||
|
attribute :team_1_id, ROM::Types::Integer.optional
|
||||||
|
attribute :team_2_id, ROM::Types::Integer.optional
|
||||||
|
attribute :bracket_position, ROM::Types::Integer.optional
|
||||||
|
attribute :winner_team_id, ROM::Types::Integer.optional
|
||||||
|
attribute :loser_team_id, ROM::Types::Integer.optional
|
||||||
|
|
||||||
|
associations do
|
||||||
|
belongs_to :event
|
||||||
|
belongs_to :round, relation: :tournament_rounds
|
||||||
|
belongs_to :match
|
||||||
|
belongs_to :team_1, relation: :teams
|
||||||
|
belongs_to :team_2, relation: :teams
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Persistence
|
||||||
|
module Relations
|
||||||
|
class EventParticipants < ROM::Relation[:sql]
|
||||||
|
schema(:event_participants, infer: true) do
|
||||||
|
attribute :id, ROM::Types::Integer, primary_key: true
|
||||||
|
attribute :event_id, ROM::Types::Integer
|
||||||
|
attribute :player_id, ROM::Types::Integer
|
||||||
|
attribute :team_id, ROM::Types::Integer.optional
|
||||||
|
attribute :status, ROM::Types::String.default('registered')
|
||||||
|
attribute :seed, ROM::Types::Integer.optional
|
||||||
|
attribute :registration_date, ROM::Types::DateTime
|
||||||
|
|
||||||
|
associations do
|
||||||
|
belongs_to :event
|
||||||
|
belongs_to :player
|
||||||
|
belongs_to :team
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Persistence
|
||||||
|
module Relations
|
||||||
|
class Events < ROM::Relation[:sql]
|
||||||
|
schema(:events, infer: true) do
|
||||||
|
attribute :id, ROM::Types::Integer, primary_key: true
|
||||||
|
attribute :event_id, ROM::Types::Integer.optional
|
||||||
|
attribute :name, ROM::Types::String
|
||||||
|
attribute :description, ROM::Types::String.optional
|
||||||
|
attribute :event_date, ROM::Types::DateTime.optional
|
||||||
|
attribute :event_type, ROM::Types::String.default('tournament')
|
||||||
|
attribute :format, ROM::Types::String.default('single_elim')
|
||||||
|
attribute :status, ROM::Types::String.default('planned')
|
||||||
|
attribute :max_participants, ROM::Types::Integer.optional
|
||||||
|
attribute :created_at, ROM::Types::DateTime
|
||||||
|
attribute :updated_at, ROM::Types::DateTime
|
||||||
|
|
||||||
|
associations do
|
||||||
|
has_many :event_participants
|
||||||
|
has_many :teams
|
||||||
|
has_many :tournament_rounds
|
||||||
|
has_many :bracket_matchups
|
||||||
|
has_many :matches
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Persistence
|
||||||
|
module Relations
|
||||||
|
class PartnershipGames < ROM::Relation[:sql]
|
||||||
|
schema(:partnership_games, infer: true) do
|
||||||
|
attribute :id, ROM::Types::Integer, primary_key: true
|
||||||
|
attribute :match_id, ROM::Types::Integer
|
||||||
|
attribute :player_1_id, ROM::Types::Integer
|
||||||
|
attribute :player_2_id, ROM::Types::Integer
|
||||||
|
attribute :team_number, ROM::Types::Integer
|
||||||
|
attribute :won_match, ROM::Types::Integer
|
||||||
|
attribute :created_at, ROM::Types::DateTime
|
||||||
|
|
||||||
|
associations do
|
||||||
|
belongs_to :match
|
||||||
|
belongs_to :player_1, relation: :players, foreign_key: :player_1_id
|
||||||
|
belongs_to :player_2, relation: :players, foreign_key: :player_2_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Persistence
|
||||||
|
module Relations
|
||||||
|
class PartnershipStats < ROM::Relation[:sql]
|
||||||
|
schema(:partnership_stats, infer: true) do
|
||||||
|
attribute :id, ROM::Types::Integer, primary_key: true
|
||||||
|
attribute :player_1_id, ROM::Types::Integer
|
||||||
|
attribute :player_2_id, ROM::Types::Integer
|
||||||
|
attribute :games_played, ROM::Types::Integer.default(0)
|
||||||
|
attribute :wins, ROM::Types::Integer.default(0)
|
||||||
|
attribute :losses, ROM::Types::Integer.default(0)
|
||||||
|
attribute :total_elo_change, ROM::Types::Integer.default(0)
|
||||||
|
attribute :last_played, ROM::Types::DateTime.optional
|
||||||
|
attribute :created_at, ROM::Types::DateTime
|
||||||
|
attribute :updated_at, ROM::Types::DateTime
|
||||||
|
|
||||||
|
associations do
|
||||||
|
belongs_to :player_1, relation: :players, foreign_key: :player_1_id
|
||||||
|
belongs_to :player_2, relation: :players, foreign_key: :player_2_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Persistence
|
||||||
|
module Relations
|
||||||
|
class Teams < ROM::Relation[:sql]
|
||||||
|
schema(:teams, infer: true) do
|
||||||
|
attribute :id, ROM::Types::Integer, primary_key: true
|
||||||
|
attribute :event_id, ROM::Types::Integer
|
||||||
|
attribute :team_name, ROM::Types::String.optional
|
||||||
|
attribute :player_1_id, ROM::Types::Integer
|
||||||
|
attribute :player_2_id, ROM::Types::Integer
|
||||||
|
attribute :created_at, ROM::Types::DateTime
|
||||||
|
|
||||||
|
associations do
|
||||||
|
belongs_to :event
|
||||||
|
belongs_to :player_1, relation: :players, foreign_key: :player_1_id
|
||||||
|
belongs_to :player_2, relation: :players, foreign_key: :player_2_id
|
||||||
|
has_many :event_participants
|
||||||
|
has_many :bracket_matchups
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Persistence
|
||||||
|
module Relations
|
||||||
|
class TournamentRounds < ROM::Relation[:sql]
|
||||||
|
schema(:tournament_rounds, infer: true) do
|
||||||
|
attribute :id, ROM::Types::Integer, primary_key: true
|
||||||
|
attribute :event_id, ROM::Types::Integer
|
||||||
|
attribute :round_number, ROM::Types::Integer
|
||||||
|
attribute :status, ROM::Types::String.default('pending')
|
||||||
|
attribute :scheduled_start, ROM::Types::DateTime.optional
|
||||||
|
attribute :actual_start, ROM::Types::DateTime.optional
|
||||||
|
attribute :completed_at, ROM::Types::DateTime.optional
|
||||||
|
|
||||||
|
associations do
|
||||||
|
belongs_to :event
|
||||||
|
has_many :bracket_matchups
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Tournament Partnership Analytics', type: :acceptance do
|
||||||
|
let(:rom) { EuchreCamp::App['persistence.rom'] }
|
||||||
|
let(:db) { rom.gateways[:default].connection }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Clear all data before each test in correct order (due to foreign keys)
|
||||||
|
db[:partnership_games].delete
|
||||||
|
db[:partnership_stats].delete
|
||||||
|
db[:elo_snapshots].delete
|
||||||
|
db[:bracket_matchups].delete
|
||||||
|
db[:tournament_rounds].delete
|
||||||
|
db[:matches].delete
|
||||||
|
db[:teams].delete
|
||||||
|
db[:event_participants].delete
|
||||||
|
db[:events].delete
|
||||||
|
db[:players].delete
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Tournament Creation and Match Recording' do
|
||||||
|
let!(:emma) { db[:players].insert(name: 'Emma', rating: 1000, current_elo: 1000) }
|
||||||
|
let!(:alice) { db[:players].insert(name: 'Alice', rating: 1000, current_elo: 1000) }
|
||||||
|
let!(:bob) { db[:players].insert(name: 'Bob', rating: 1000, current_elo: 1000) }
|
||||||
|
let!(:charlie) { db[:players].insert(name: 'Charlie', rating: 1000, current_elo: 1000) }
|
||||||
|
let!(:david) { db[:players].insert(name: 'David', rating: 1000, current_elo: 1000) }
|
||||||
|
|
||||||
|
it 'creates a tournament with teams and matches' do
|
||||||
|
# Create tournament
|
||||||
|
tournament_id = db[:events].insert(
|
||||||
|
name: 'Test Tournament',
|
||||||
|
format: 'round_robin',
|
||||||
|
status: 'active'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create teams
|
||||||
|
team_1_id = db[:teams].insert(
|
||||||
|
event_id: tournament_id,
|
||||||
|
team_name: 'Ace High',
|
||||||
|
player_1_id: emma,
|
||||||
|
player_2_id: alice
|
||||||
|
)
|
||||||
|
team_2_id = db[:teams].insert(
|
||||||
|
event_id: tournament_id,
|
||||||
|
team_name: 'Kings',
|
||||||
|
player_1_id: bob,
|
||||||
|
player_2_id: charlie
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a match
|
||||||
|
match_id = db[:matches].insert(
|
||||||
|
event_id: tournament_id,
|
||||||
|
team_1_p1_id: emma,
|
||||||
|
team_1_p2_id: alice,
|
||||||
|
team_2_p1_id: bob,
|
||||||
|
team_2_p2_id: charlie,
|
||||||
|
team_1_score: 10,
|
||||||
|
team_2_score: 7,
|
||||||
|
status: 'completed'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify match was created
|
||||||
|
match = db[:matches].where(id: match_id).first
|
||||||
|
expect(match[:team_1_p1_id]).to eq(emma)
|
||||||
|
expect(match[:team_1_p2_id]).to eq(alice)
|
||||||
|
expect(match[:team_2_p1_id]).to eq(bob)
|
||||||
|
expect(match[:team_2_p2_id]).to eq(charlie)
|
||||||
|
expect(match[:team_1_score]).to eq(10)
|
||||||
|
expect(match[:team_2_score]).to eq(7)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates Elo for a match' do
|
||||||
|
match_id = db[:matches].insert(
|
||||||
|
team_1_p1_id: emma,
|
||||||
|
team_1_p2_id: alice,
|
||||||
|
team_2_p1_id: bob,
|
||||||
|
team_2_p2_id: charlie,
|
||||||
|
team_1_score: 10,
|
||||||
|
team_2_score: 7,
|
||||||
|
status: 'completed'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run the Elo calculation job
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.new.perform(match_id)
|
||||||
|
|
||||||
|
# Verify Elo snapshots were created for all 4 players
|
||||||
|
snapshots = db[:elo_snapshots].where(match_id: match_id).to_a
|
||||||
|
expect(snapshots.length).to eq(4)
|
||||||
|
|
||||||
|
# Verify player ELOs were updated
|
||||||
|
emma_player = db[:players].where(id: emma).first
|
||||||
|
expect(emma_player[:current_elo]).not_to eq(1000)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'records partnerships when match completes' do
|
||||||
|
match_id = db[:matches].insert(
|
||||||
|
team_1_p1_id: emma,
|
||||||
|
team_1_p2_id: alice,
|
||||||
|
team_2_p1_id: bob,
|
||||||
|
team_2_p2_id: charlie,
|
||||||
|
team_1_score: 10,
|
||||||
|
team_2_score: 7,
|
||||||
|
status: 'completed'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run the full job (Elo + partnerships)
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.new.perform(match_id)
|
||||||
|
|
||||||
|
# Verify partnership games were recorded
|
||||||
|
partnership_games = db[:partnership_games].where(match_id: match_id).to_a
|
||||||
|
expect(partnership_games.length).to eq(2) # One for each team
|
||||||
|
|
||||||
|
# Verify team 1 partnership (emma + alice) won
|
||||||
|
team_1_game = partnership_games.find { |pg| pg[:team_number] == 1 }
|
||||||
|
expect(team_1_game[:player_1_id]).to eq(emma)
|
||||||
|
expect(team_1_game[:player_2_id]).to eq(alice)
|
||||||
|
expect(team_1_game[:won_match]).to eq(1)
|
||||||
|
|
||||||
|
# Verify team 2 partnership (bob + charlie) lost
|
||||||
|
team_2_game = partnership_games.find { |pg| pg[:team_number] == 2 }
|
||||||
|
expect(team_2_game[:player_1_id]).to eq(bob)
|
||||||
|
expect(team_2_game[:player_2_id]).to eq(charlie)
|
||||||
|
expect(team_2_game[:won_match]).to eq(0)
|
||||||
|
|
||||||
|
# Verify partnership stats were updated
|
||||||
|
stats = db[:partnership_stats].to_a
|
||||||
|
expect(stats.length).to eq(2)
|
||||||
|
|
||||||
|
# Check emma+alice stats
|
||||||
|
emma_alice_stats = stats.find do |s|
|
||||||
|
(s[:player_1_id] == emma && s[:player_2_id] == alice) ||
|
||||||
|
(s[:player_1_id] == alice && s[:player_2_id] == emma)
|
||||||
|
end
|
||||||
|
expect(emma_alice_stats[:games_played]).to eq(1)
|
||||||
|
expect(emma_alice_stats[:wins]).to eq(1)
|
||||||
|
expect(emma_alice_stats[:losses]).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tracks multiple partnership statistics correctly' do
|
||||||
|
# Match 1: emma+alice wins
|
||||||
|
match_1_id = db[:matches].insert(
|
||||||
|
team_1_p1_id: emma, team_1_p2_id: alice,
|
||||||
|
team_2_p1_id: bob, team_2_p2_id: charlie,
|
||||||
|
team_1_score: 10, team_2_score: 7, status: 'completed'
|
||||||
|
)
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.new.perform(match_1_id)
|
||||||
|
|
||||||
|
# Match 2: emma+alice wins again
|
||||||
|
match_2_id = db[:matches].insert(
|
||||||
|
team_1_p1_id: emma, team_1_p2_id: alice,
|
||||||
|
team_2_p1_id: david, team_2_p2_id: bob,
|
||||||
|
team_1_score: 10, team_2_score: 5, status: 'completed'
|
||||||
|
)
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.new.perform(match_2_id)
|
||||||
|
|
||||||
|
# Match 3: emma+alice loses
|
||||||
|
match_3_id = db[:matches].insert(
|
||||||
|
team_1_p1_id: emma, team_1_p2_id: alice,
|
||||||
|
team_2_p1_id: bob, team_2_p2_id: charlie,
|
||||||
|
team_1_score: 7, team_2_score: 10, status: 'completed'
|
||||||
|
)
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.new.perform(match_3_id)
|
||||||
|
|
||||||
|
# Check emma+alice stats
|
||||||
|
emma_alice_stats = db[:partnership_stats].find do |s|
|
||||||
|
(s[:player_1_id] == emma && s[:player_2_id] == alice) ||
|
||||||
|
(s[:player_1_id] == alice && s[:player_2_id] == emma)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(emma_alice_stats[:games_played]).to eq(3)
|
||||||
|
expect(emma_alice_stats[:wins]).to eq(2)
|
||||||
|
expect(emma_alice_stats[:losses]).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates partnership win rates correctly' do
|
||||||
|
# Create 5 matches for emma+alice with 3 wins, 2 losses
|
||||||
|
matches = [
|
||||||
|
{ p1: emma, p2: alice, opp1: bob, opp2: charlie, score1: 10, score2: 7 },
|
||||||
|
{ p1: emma, p2: alice, opp1: david, opp2: bob, score1: 10, score2: 5 },
|
||||||
|
{ p1: emma, p2: alice, opp1: bob, opp2: charlie, score1: 7, score2: 10 },
|
||||||
|
{ p1: emma, p2: alice, opp1: david, opp2: charlie, score1: 10, score2: 8 },
|
||||||
|
{ p1: emma, p2: alice, opp1: bob, opp2: david, score1: 6, score2: 10 },
|
||||||
|
]
|
||||||
|
|
||||||
|
matches.each do |m|
|
||||||
|
match_id = db[:matches].insert(
|
||||||
|
team_1_p1_id: m[:p1], team_1_p2_id: m[:p2],
|
||||||
|
team_2_p1_id: m[:opp1], team_2_p2_id: m[:opp2],
|
||||||
|
team_1_score: m[:score1], team_2_score: m[:score2],
|
||||||
|
status: 'completed'
|
||||||
|
)
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.new.perform(match_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check stats
|
||||||
|
stats = db[:partnership_stats].find do |s|
|
||||||
|
(s[:player_1_id] == emma && s[:player_2_id] == alice) ||
|
||||||
|
(s[:player_1_id] == alice && s[:player_2_id] == emma)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(stats[:games_played]).to eq(5)
|
||||||
|
expect(stats[:wins]).to eq(3)
|
||||||
|
expect(stats[:losses]).to eq(2)
|
||||||
|
|
||||||
|
# Calculate expected win rate
|
||||||
|
win_rate = stats[:wins].to_f / stats[:games_played]
|
||||||
|
expect(win_rate).to be_within(0.01).of(0.600)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'retrieves partnership statistics for a player' do
|
||||||
|
# Create matches with different partners for emma
|
||||||
|
matches_data = [
|
||||||
|
# emma+alice (2 wins, 1 loss)
|
||||||
|
{ team_1: [emma, alice], team_2: [bob, charlie], score1: 10, score2: 7 },
|
||||||
|
{ team_1: [emma, alice], team_2: [david, bob], score1: 10, score2: 5 },
|
||||||
|
{ team_1: [emma, alice], team_2: [bob, charlie], score1: 7, score2: 10 },
|
||||||
|
# emma+bob (1 win, 0 losses)
|
||||||
|
{ team_1: [emma, bob], team_2: [alice, charlie], score1: 10, score2: 6 },
|
||||||
|
]
|
||||||
|
|
||||||
|
matches_data.each do |data|
|
||||||
|
match_id = db[:matches].insert(
|
||||||
|
team_1_p1_id: data[:team_1][0], team_1_p2_id: data[:team_1][1],
|
||||||
|
team_2_p1_id: data[:team_2][0], team_2_p2_id: data[:team_2][1],
|
||||||
|
team_1_score: data[:score1], team_2_score: data[:score2],
|
||||||
|
status: 'completed'
|
||||||
|
)
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.new.perform(match_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get partnerships for emma
|
||||||
|
partnerships = EuchreCamp::Services::PartnershipTracker.get_player_partnerships(emma)
|
||||||
|
|
||||||
|
expect(partnerships.length).to eq(2)
|
||||||
|
|
||||||
|
# Check alice partnership
|
||||||
|
alice_partner = partnerships.find { |p| p[:partner][:id] == alice }
|
||||||
|
expect(alice_partner).not_to be_nil
|
||||||
|
expect(alice_partner[:games_played]).to eq(3)
|
||||||
|
expect(alice_partner[:wins]).to eq(2)
|
||||||
|
expect(alice_partner[:losses]).to eq(1)
|
||||||
|
expect(alice_partner[:win_rate]).to be_within(0.01).of(0.667)
|
||||||
|
|
||||||
|
# Check bob partnership
|
||||||
|
bob_partner = partnerships.find { |p| p[:partner][:id] == bob }
|
||||||
|
expect(bob_partner).not_to be_nil
|
||||||
|
expect(bob_partner[:games_played]).to eq(1)
|
||||||
|
expect(bob_partner[:wins]).to eq(1)
|
||||||
|
expect(bob_partner[:losses]).to eq(0)
|
||||||
|
expect(bob_partner[:win_rate]).to eq(1.0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Round Robin Tournament Workflow' do
|
||||||
|
let!(:emma) { db[:players].insert(name: 'Emma', rating: 1000, current_elo: 1000) }
|
||||||
|
let!(:alice) { db[:players].insert(name: 'Alice', rating: 1000, current_elo: 1000) }
|
||||||
|
let!(:bob) { db[:players].insert(name: 'Bob', rating: 1000, current_elo: 1000) }
|
||||||
|
let!(:charlie) { db[:players].insert(name: 'Charlie', rating: 1000, current_elo: 1000) }
|
||||||
|
|
||||||
|
it 'completes a full round-robin tournament workflow' do
|
||||||
|
# 1. Create tournament
|
||||||
|
tournament_id = db[:events].insert(
|
||||||
|
name: 'Spring Championship',
|
||||||
|
format: 'round_robin',
|
||||||
|
status: 'active'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Register participants
|
||||||
|
[emma, alice, bob, charlie].each do |player_id|
|
||||||
|
db[:event_participants].insert(
|
||||||
|
event_id: tournament_id,
|
||||||
|
player_id: player_id,
|
||||||
|
status: 'registered'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 3. Create teams (4 teams for round robin)
|
||||||
|
teams = [
|
||||||
|
['Ace High', emma, alice],
|
||||||
|
['Kings', bob, charlie],
|
||||||
|
['Queen High', emma, bob],
|
||||||
|
['Jacks', alice, charlie]
|
||||||
|
]
|
||||||
|
|
||||||
|
team_ids = teams.map do |name, p1, p2|
|
||||||
|
db[:teams].insert(event_id: tournament_id, team_name: name, player_1_id: p1, player_2_id: p2)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 4. Generate schedule (using TournamentGenerator)
|
||||||
|
rounds = EuchreCamp::Services::TournamentGenerator.round_robin(
|
||||||
|
teams.map.with_index { |t, i| { id: team_ids[i], name: t[0] } }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(rounds.length).to eq(3) # 4 teams = 3 rounds
|
||||||
|
|
||||||
|
# 5. Create rounds and matchups
|
||||||
|
rounds.each do |round_data|
|
||||||
|
round_id = db[:tournament_rounds].insert(
|
||||||
|
event_id: tournament_id,
|
||||||
|
round_number: round_data[:round_number],
|
||||||
|
status: round_data[:round_number] == 1 ? 'active' : 'pending'
|
||||||
|
)
|
||||||
|
|
||||||
|
round_data[:matchups].each_with_index do |matchup, idx|
|
||||||
|
db[:bracket_matchups].insert(
|
||||||
|
event_id: tournament_id,
|
||||||
|
round_id: round_id,
|
||||||
|
team_1_id: matchup[:team_1_id],
|
||||||
|
team_2_id: matchup[:team_2_id],
|
||||||
|
bracket_position: idx + 1
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# 6. Play round 1 matches
|
||||||
|
round_1 = db[:tournament_rounds].where(round_number: 1, event_id: tournament_id).first
|
||||||
|
round_1_matchups = db[:bracket_matchups].where(round_id: round_1[:id]).to_a
|
||||||
|
|
||||||
|
round_1_matchups.each do |matchup|
|
||||||
|
team_1 = db[:teams].where(id: matchup[:team_1_id]).first
|
||||||
|
team_2 = db[:teams].where(id: matchup[:team_2_id]).first
|
||||||
|
|
||||||
|
match_id = db[:matches].insert(
|
||||||
|
event_id: tournament_id,
|
||||||
|
team_1_p1_id: team_1[:player_1_id],
|
||||||
|
team_1_p2_id: team_1[:player_2_id],
|
||||||
|
team_2_p1_id: team_2[:player_1_id],
|
||||||
|
team_2_p2_id: team_2[:player_2_id],
|
||||||
|
team_1_score: 10,
|
||||||
|
team_2_score: 7,
|
||||||
|
status: 'completed'
|
||||||
|
)
|
||||||
|
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.new.perform(match_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 7. Verify all partnerships were recorded
|
||||||
|
partnership_games = db[:partnership_games].to_a
|
||||||
|
# For 2 matchups in round 1, we should have 4 partnership games (2 per matchup)
|
||||||
|
# But if emma appears in multiple teams, we might see fewer unique games
|
||||||
|
expect(partnership_games.length).to be >= 2
|
||||||
|
|
||||||
|
# 8. Verify player profiles can be retrieved
|
||||||
|
emma_partnerships = EuchreCamp::Services::PartnershipTracker.get_player_partnerships(emma)
|
||||||
|
expect(emma_partnerships.length).to be >= 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Player Profile Page' do
|
||||||
|
let!(:emma) { db[:players].insert(name: 'Emma', rating: 1000, current_elo: 1000) }
|
||||||
|
let!(:alice) { db[:players].insert(name: 'Alice', rating: 1000, current_elo: 1000) }
|
||||||
|
|
||||||
|
it 'displays player profile with partnership data' do
|
||||||
|
# Create additional players for opponents
|
||||||
|
bob = db[:players].insert(name: 'Bob', rating: 1000, current_elo: 1000)
|
||||||
|
charlie = db[:players].insert(name: 'Charlie', rating: 1000, current_elo: 1000)
|
||||||
|
|
||||||
|
# Create some matches
|
||||||
|
5.times do |i|
|
||||||
|
match_id = db[:matches].insert(
|
||||||
|
team_1_p1_id: emma, team_1_p2_id: alice,
|
||||||
|
team_2_p1_id: bob, team_2_p2_id: charlie,
|
||||||
|
team_1_score: 10, team_2_score: 7,
|
||||||
|
status: 'completed'
|
||||||
|
)
|
||||||
|
EuchreCamp::Jobs::CalculateEloJob.new.perform(match_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get partnerships for emma
|
||||||
|
partnerships = EuchreCamp::Services::PartnershipTracker.get_player_partnerships(emma)
|
||||||
|
|
||||||
|
# Verify data structure
|
||||||
|
expect(partnerships).not_to be_empty
|
||||||
|
partnership = partnerships.first
|
||||||
|
expect(partnership[:partner][:name]).to eq('Alice')
|
||||||
|
expect(partnership[:games_played]).to eq(5)
|
||||||
|
expect(partnership[:wins]).to eq(5)
|
||||||
|
expect(partnership[:losses]).to eq(0)
|
||||||
|
expect(partnership[:win_rate]).to eq(1.0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user