--- /dev/null
+#!/usr/bin/ruby
+
+=begin
+What drives popularity? Are Pareto popularity distributions possible? likely?
+guaranteed?
+=end
+
+class Array
+ def pick_one
+ return nil if empty?
+ self[rand(self.length)]
+ end
+end
+
+class Popularity
+ # A Character has a name and nothing more
+ class Character
+ attr :name
+ def initialize(name)
+ @name = name.intern
+ end
+ end
+
+ # A Star is just a Character, no special abilities
+ class Star < Character ; end
+
+ # A Commoner is a Character with one or more voting strategies
+ class Commoner < Character
+ # The simplest voting strategy: pick a random one
+ def random_vote(popularity, total)
+ popularity.keys.pick_one
+ end
+
+ # A voting strategy that picks a random star, weighting
+ # more popular ones more
+ def popular_vote(popularity, total)
+ # if nobody voted yet, pick a random one
+ return random_vote(popularity, total) if total == 0
+ pick = rand(total)
+ popularity.each do |star, pop|
+ return star if pick < pop
+ pick -= pop
+ end
+ end
+
+ end
+
+ # Add a new Star
+ def add_star(n)
+ case n
+ when String
+ name = n
+ when Integer
+ name = "Star #%u" % n
+ else
+ raise ArgumentError, "#{n} is neither a String nor an Integer"
+ end
+
+ @stars[name.intern] = Star.new(name)
+ end
+
+ # Add a new Commoner
+ def add_commoner(n)
+ case n
+ when String
+ name = n
+ when Integer
+ name = "Commoner #%u" % n
+ else
+ raise ArgumentError, "#{n} is neither a String nor an Integer"
+ end
+
+ @commoners[name.intern] = Commoner.new(name)
+ end
+
+ # Today
+ def today ; @day ; end
+ # Yesterday
+ def yesterday
+ raise if @day == 0
+ @day-1
+ end
+
+ # Initialize a new popularity day
+ def push_popularity
+ @popularity << Hash.new do |h, k|
+ raise ArgumentError, "no such Star '#{k}'" unless @stars.key? k
+ h[k] = []
+ end
+ end
+
+ def new_day
+ @day += 1
+ push_popularity
+ end
+
+ def initialize
+ @stars = {}
+ @commoners = {}
+ @popularity = []
+ reset
+ end
+
+ def reset
+ @day = 0
+ @popularity.clear
+ push_popularity
+ end
+
+ def stars ; @stars.keys ; end
+ def commoners ; @commoners.keys ; end
+
+ # Get :day's [default: today] popularity for :stars [default: all stars].
+ # Returns:
+ # a hash star => number of commoners that voted for it that day
+ def popularity(o={})
+ pop = @popularity[o[:day] || today]
+ sel = {}
+ [o[:stars] || stars].flatten.each do |s|
+ sel[s] = pop[s].length
+ end
+ return sel
+ end
+
+ # an Array of [star, popularity] ordered by popularity
+ def grade(o={})
+ popularity(o).to_a.sort { |s1, s2| s2.last <=> s1.last }
+ end
+
+ def vote(o = {})
+ pop_org = popularity(:day => yesterday)
+ # to allow giving stars with no popularity a chance,
+ # we allow a :mul and :add option to the voting session.
+ mul = o[:mul] || 1
+ add = o[:add] || 0
+ tot = 0
+ pop = {}
+ pop_org.each do |star, p|
+ pop[star] = p*mul + add
+ tot += pop[star]
+ end
+ @commoners.each do |name, commoner|
+ star = commoner.popular_vote(pop, tot)
+ @popularity[today][star] << name
+ end
+ end
+
+end
+
+pop = Popularity.new
+
+5.times do |i| pop.add_star(i) end
+100.times do |i| pop.add_commoner(i) end
+
+puts pop.grade.map { |s, p| "#{s} (#{p})" }.join(', ')
+
+puts
+1000.times do
+ pop.new_day
+ pop.vote
+ puts pop.grade.map { |s, p| "#{s} (#{p})" }.join(', ')
+end
+
+puts
+pop.reset
+1000.times do
+ pop.new_day
+ pop.vote(:add => 1)
+ puts pop.grade.map { |s, p| "#{s} (#{p})" }.join(', ')
+end
+
+puts
+pop.reset
+1000.times do
+ pop.new_day
+ pop.vote(:mul => 10, :add => 1)
+ puts pop.grade.map { |s, p| "#{s} (#{p})" }.join(', ')
+end
+