1268889a78
- Implement EloCalculator service for 2v2 Euchre matches - Create background job processing with GoodJob - Add single match entry form - Add CSV upload for batch match entry - Create player rankings view - Add database schema for matches and elo snapshots
81 lines
2.9 KiB
Ruby
81 lines
2.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module EuchreCamp
|
|
module Services
|
|
class EloCalculator
|
|
# Standard Elo K-factor
|
|
K_FACTOR = 32
|
|
|
|
# Calculate Elo ratings for a 2v2 Euchre match
|
|
#
|
|
# @param player_ratings [Hash] player_id => current_rating
|
|
# @param team_1_players [Array<Integer>] player IDs for team 1
|
|
# @param team_2_players [Array<Integer>] player IDs for team 2
|
|
# @param team_1_score [Integer] score for team 1
|
|
# @param team_2_score [Integer] score for team 2
|
|
# @return [Hash] player_id => { rating_before:, rating_after:, rating_change: }
|
|
def self.calculate(player_ratings:, team_1_players:, team_2_players:, team_1_score:, team_2_score:)
|
|
# Validate inputs
|
|
raise ArgumentError, "Team 1 must have exactly 2 players" unless team_1_players.length == 2
|
|
raise ArgumentError, "Team 2 must have exactly 2 players" unless team_2_players.length == 2
|
|
|
|
# Determine winner (first to 10 points)
|
|
team_1_wins = team_1_score >= 10
|
|
team_2_wins = team_2_score >= 10
|
|
|
|
unless team_1_wins || team_2_wins
|
|
raise ArgumentError, "One team must reach 10 points to win"
|
|
end
|
|
|
|
if team_1_wins && team_2_wins
|
|
raise ArgumentError, "Only one team can win (both reached 10 points)"
|
|
end
|
|
|
|
winner = team_1_wins ? :team_1 : :team_2
|
|
|
|
# Calculate average ratings for each team
|
|
team_1_avg = team_1_players.map { |id| player_ratings[id] }.sum / 2.0
|
|
team_2_avg = team_2_players.map { |id| player_ratings[id] }.sum / 2.0
|
|
|
|
# Calculate expected scores using Elo formula
|
|
# E = 1 / (1 + 10^((R_opp - R_team) / 400))
|
|
expected_team_1 = 1.0 / (1.0 + 10.0**((team_2_avg - team_1_avg) / 400.0))
|
|
expected_team_2 = 1.0 / (1.0 + 10.0**((team_1_avg - team_2_avg) / 400.0))
|
|
|
|
# Actual scores (1 for win, 0 for loss)
|
|
actual_team_1 = team_1_wins ? 1.0 : 0.0
|
|
actual_team_2 = team_2_wins ? 1.0 : 0.0
|
|
|
|
# Calculate rating changes for each player
|
|
results = {}
|
|
|
|
# Team 1 players
|
|
team_1_players.each do |player_id|
|
|
old_rating = player_ratings[player_id]
|
|
rating_change = K_FACTOR * (actual_team_1 - expected_team_1)
|
|
new_rating = (old_rating + rating_change).round
|
|
results[player_id] = {
|
|
rating_before: old_rating,
|
|
rating_after: new_rating,
|
|
rating_change: rating_change.round
|
|
}
|
|
end
|
|
|
|
# Team 2 players
|
|
team_2_players.each do |player_id|
|
|
old_rating = player_ratings[player_id]
|
|
rating_change = K_FACTOR * (actual_team_2 - expected_team_2)
|
|
new_rating = (old_rating + rating_change).round
|
|
results[player_id] = {
|
|
rating_before: old_rating,
|
|
rating_after: new_rating,
|
|
rating_change: rating_change.round
|
|
}
|
|
end
|
|
|
|
results
|
|
end
|
|
end
|
|
end
|
|
end
|