|
|
# 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
|