You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

74 lines
2.6 KiB

# Computes the country leaderboard using a blended score that balances overall
# impact with per-employee efficiency, so bigger teams don't win on size alone.
#
# For each country:
# tomatoes = number of finished play sessions (players where is_done)
# per_employee = tomatoes / employee headcount (nil if headcount unknown)
# impact_index = tomatoes / max(tomatoes) * 100
# efficiency = per_employee / max(per_employee) * 100
# score = impact_index * IMPACT_WEIGHT + efficiency * EFFICIENCY_WEIGHT
#
# Tune the balance here. The proposal's default is an even 50/50 split; raise
# EFFICIENCY_WEIGHT to lean more toward fairness across team sizes.
class LeaderboardScore
IMPACT_WEIGHT = 0.5
EFFICIENCY_WEIGHT = 1.0 - IMPACT_WEIGHT
def self.call
new.call
end
def call
rows = build_rows
max_tomatoes = rows.map { |r| r[:tomatoes] }.max.to_f
max_per_emp = rows.filter_map { |r| r[:per_employee] }.max.to_f
rows.map { |r| score_row(r, max_tomatoes, max_per_emp) }
.sort_by { |r| [ -r[:score], r[:country_code] ] }
end
private
# One row per known country, so the API always lists every country. Only
# players that are done AND tagged with a known country contribute tomatoes;
# countries with no finished players still appear, with 0 tomatoes / 0 score.
def build_rows
known = I18n.t("countries").keys.map(&:to_s)
counts = Player.where(is_done: true, country: known).group(:country).count
headcounts = self.class.headcounts
known.map do |country|
tomatoes = counts[country].to_i
employees = headcounts[country].to_i
per_employee = employees.positive? ? tomatoes.to_f / employees : nil
{ country: country, tomatoes: tomatoes, per_employee: per_employee }
end
end
def score_row(row, max_tomatoes, max_per_emp)
impact = max_tomatoes.positive? ? row[:tomatoes] / max_tomatoes * 100 : 0.0
efficiency = if row[:per_employee] && max_per_emp.positive?
row[:per_employee] / max_per_emp * 100
else
# No headcount for this country: fall back to impact only rather than
# penalising it with a zero efficiency score.
impact
end
blended = impact * IMPACT_WEIGHT + efficiency * EFFICIENCY_WEIGHT
{
country_code: row[:country].to_s.upcase,
score: blended.round,
tomatoes: row[:tomatoes],
per_employee: row[:per_employee]&.round(2)
}
end
def self.headcounts
@headcounts ||= YAML.safe_load_file(
Rails.root.join("config", "country_headcounts.yml"),
permitted_classes: [], aliases: false
) || {}
end
end