# 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 !" bottom 10 featured countries # #2 new_players "Yay! New players in " countries with recent sign-ups # " new players in !" (benchmark variant) # #3 participation ", 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