# Builds the rotating banner "badges" that sit alongside the ranked leaderboard.
|
|
# The banner picks one at a time or cycles through the whole list.
|
|
#
|
|
# Three kinds, matching the client's brief:
|
|
# #1 encouragement "Ketch-up <Country>!" bottom 10 featured countries
|
|
# #2 new_players "Yay! New players in <Country>" countries with recent sign-ups
|
|
# "<N> new players in <Country>!" (benchmark variant)
|
|
# #3 participation "<Country>, every tomato counts!" everyone not covered above
|
|
#
|
|
# Tunables — the client's example numbers, adjust freely:
|
|
class LeaderboardBadges
|
|
NEW_PLAYER_WINDOW = 24.hours # #2: "new players in the last X hours"
|
|
BENCHMARKS = [100, 50, 10] # #2: highest crossed threshold wins
|
|
ENCOURAGEMENT_COUNT = 10 # #1: rotate across the bottom N
|
|
|
|
# leaderboard: the array returned by LeaderboardScore.call (already ranked).
|
|
def self.call(leaderboard)
|
|
new(leaderboard).call
|
|
end
|
|
|
|
def initialize(leaderboard)
|
|
@leaderboard = leaderboard
|
|
end
|
|
|
|
# At most three badges — one random pick of each type. The pick changes on
|
|
# each cache refresh, so the banner naturally rotates through countries.
|
|
def call
|
|
[
|
|
new_player_badges.sample,
|
|
encouragement_badges.sample,
|
|
participation_badges.sample
|
|
].compact
|
|
end
|
|
|
|
private
|
|
|
|
# #2 — countries that gained new players (started sessions) within the window.
|
|
def new_player_badges
|
|
new_player_counts.map do |code, count|
|
|
benchmark = BENCHMARKS.find { |b| count >= b }
|
|
message = if benchmark
|
|
"#{benchmark} new players in #{country_name(code)}!"
|
|
else
|
|
"Yay! New players in #{country_name(code)}"
|
|
end
|
|
badge(:new_players, code, message)
|
|
end
|
|
end
|
|
|
|
# #1 — encouragement for the bottom N countries that are actually playing.
|
|
def encouragement_badges
|
|
bottom_featured.map do |code|
|
|
badge(:encouragement, code, "Ketch-up #{country_name(code)}!")
|
|
end
|
|
end
|
|
|
|
# #3 — everyone not featured on the leaderboard and not covered by #1 or #2.
|
|
def participation_badges
|
|
covered = (new_player_counts.keys + bottom_featured).to_set
|
|
unfeatured_codes.reject { |code| covered.include?(code) }.map do |code|
|
|
badge(:participation, code, "#{country_name(code)}, every tomato counts!")
|
|
end
|
|
end
|
|
|
|
# Countries with at least one tomato — the ones "on" the leaderboard.
|
|
def featured
|
|
@featured ||= @leaderboard.select { |row| row[:tomatoes].to_i.positive? }
|
|
end
|
|
|
|
# @leaderboard is ranked high→low, so the last N featured are the bottom N.
|
|
def bottom_featured
|
|
@bottom_featured ||= featured.last(ENCOURAGEMENT_COUNT).map { |row| code_of(row) }
|
|
end
|
|
|
|
def unfeatured_codes
|
|
featured_set = featured.map { |row| code_of(row) }.to_set
|
|
@leaderboard.map { |row| code_of(row) }.reject { |code| featured_set.include?(code) }
|
|
end
|
|
|
|
def new_player_counts
|
|
@new_player_counts ||= Player
|
|
.where(country: known_codes)
|
|
.where(created_at: NEW_PLAYER_WINDOW.ago..)
|
|
.group(:country).count
|
|
end
|
|
|
|
def known_codes
|
|
@known_codes ||= I18n.t("countries").keys.map(&:to_s)
|
|
end
|
|
|
|
def code_of(row)
|
|
row[:country_code].to_s.downcase
|
|
end
|
|
|
|
def country_name(code)
|
|
I18n.t("countries.#{code}", default: code.to_s.upcase)
|
|
end
|
|
|
|
def badge(type, code, message)
|
|
{ type: type.to_s, country_code: code.to_s.upcase, message: message }
|
|
end
|
|
end
|