ac858f1e6d
Implement comprehensive partnership tracking system and tournament management features including round-robin, single/double elimination, and Swiss system pairings. Changes: - Add partnership_games and partnership_stats tables - Implement PartnershipTracker service for recording partnership data - Add tournament generator with multiple formats - Create tournament management actions and views - Add acceptance tests for partnership tracking - Fix PartnershipTracker to use single snapshot per player per match - Fix round-robin test to query by round_number instead of hardcoded ID Note: All 8 acceptance tests now passing
385 lines
14 KiB
Ruby
385 lines
14 KiB
Ruby
# 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
|