class ActiveGenie::Ranking::EloRound
def self.call(...)
def self.call(...) new(...).call end
def battle(player_a, player_b)
def battle(player_a, player_b) ActiveGenie::Logger.with_context({ player_a_id: player_a.id, player_b_id: player_b.id }) do result = ActiveGenie::Battle.call( player_a.content, player_b.content, @criteria, config: @config ) winner, loser = case result['winner'] when 'player_a' then [player_a, player_b] when 'player_b' then [player_b, player_a] when 'draw' then [nil, nil] end [winner, loser] end end
def build_report
def build_report report = { elo_round_id:, players_in_round: players_in_round.map(&:id), battles_count: matches.size, total_tokens: @total_tokens, previous_highest_elo: @previous_highest_elo, highest_elo:, highest_elo_diff: highest_elo - @previous_highest_elo, players_elo_diff: } ActiveGenie::Logger.call({ code: :elo_round_report, **report }) report end
def calculate_new_elo(player_rating, opponent_rating, score)
def calculate_new_elo(player_rating, opponent_rating, score) expected_score = 1.0 / (1.0 + (10.0**((opponent_rating - player_rating) / 400.0))) player_rating + (K * (score - expected_score)).round end
def call
def call ActiveGenie::Logger.with_context(log_context) do matches.each do |player_a, player_b| # TODO: battle can take a while, can be parallelized winner, loser = battle(player_a, player_b) update_players_elo(winner, loser) end end build_report end
def elo_round_id
def elo_round_id relegation_tier_ids = @relegation_tier.map(&:id).join(',') defender_tier_ids = @defender_tier.map(&:id).join(',') ranking_unique_key = [relegation_tier_ids, defender_tier_ids, @criteria, @config.to_json].join('-') Digest::MD5.hexdigest(ranking_unique_key) end
def highest_elo
def highest_elo players_in_round.max_by(&:elo).elo end
def initialize(players, criteria, config: {})
def initialize(players, criteria, config: {}) @players = players @relegation_tier = players.calc_relegation_tier @defender_tier = players.calc_defender_tier @criteria = criteria @config = config @tmp_defenders = [] @total_tokens = 0 @previous_elo = players.to_h { |player| [player.id, player.elo] } @previous_highest_elo = @defender_tier.max_by(&:elo).elo end
def log_context
def log_context { elo_round_id: } end
def log_observer(log)
def log_observer(log) @total_tokens += log[:total_tokens] if log[:code] == :llm_usage end
def matches
def matches @relegation_tier.each_with_object([]) do |attack_player, matches| BATTLE_PER_PLAYER.times do matches << [attack_player, next_defense_player].shuffle end end end
def next_defense_player
def next_defense_player @tmp_defenders = @defender_tier.shuffle if @tmp_defenders.empty? @tmp_defenders.pop end
def players_elo_diff
def players_elo_diff elo_diffs = players_in_round.map do |player| [player.id, player.elo - @previous_elo[player.id]] end elo_diffs.sort_by { |_, diff| -diff }.to_h end
def players_in_round
def players_in_round @defender_tier + @relegation_tier end
def save_previous_elo
def save_previous_elo @previous_elo = @players.to_h { |player| [player.id, player.elo] } end
def update_players_elo(winner, loser)
def update_players_elo(winner, loser) return if winner.nil? || loser.nil? winner.elo = calculate_new_elo(winner.elo, loser.elo, 1) loser.elo = calculate_new_elo(loser.elo, winner.elo, 0) end