Add popularity script
[milivella] / vella_pop.rb
1 #!/usr/bin/ruby
2
3 =begin
4 What drives popularity? Are Pareto popularity distributions possible? likely?
5 guaranteed?
6 =end
7
8 class Array
9         def pick_one
10                 return nil if empty?
11                 self[rand(self.length)]
12         end
13 end
14
15 class Popularity
16         # A Character has a name and nothing more
17         class Character
18                 attr :name
19                 def initialize(name)
20                         @name = name.intern
21                 end
22         end
23
24         # A Star is just a Character, no special abilities
25         class Star < Character ; end
26
27         # A Commoner is a Character with one or more voting strategies
28         class Commoner < Character
29                 # The simplest voting strategy: pick a random one
30                 def random_vote(popularity, total)
31                         popularity.keys.pick_one
32                 end
33
34                 # A voting strategy that picks a random star, weighting
35                 # more popular ones more
36                 def popular_vote(popularity, total)
37                         # if nobody voted yet, pick a random one
38                         return random_vote(popularity, total) if total == 0
39                         pick = rand(total)
40                         popularity.each do |star, pop|
41                                 return star if pick < pop
42                                 pick -= pop
43                         end
44                 end
45
46         end
47
48         # Add a new Star
49         def add_star(n)
50                 case n
51                 when String
52                         name = n
53                 when Integer
54                         name = "Star #%u" % n
55                 else
56                         raise ArgumentError, "#{n} is neither a String nor an Integer"
57                 end
58
59                 @stars[name.intern] = Star.new(name)
60         end
61
62         # Add a new Commoner
63         def add_commoner(n)
64                 case n
65                 when String
66                         name = n
67                 when Integer
68                         name = "Commoner #%u" % n
69                 else
70                         raise ArgumentError, "#{n} is neither a String nor an Integer"
71                 end
72
73                 @commoners[name.intern] = Commoner.new(name)
74         end
75
76         # Today
77         def today ; @day ; end
78         # Yesterday
79         def yesterday
80                 raise if @day == 0
81                 @day-1
82         end
83
84         # Initialize a new popularity day
85         def push_popularity
86                 @popularity << Hash.new do |h, k|
87                         raise ArgumentError, "no such Star '#{k}'" unless @stars.key? k
88                         h[k] = []
89                 end
90         end
91
92         def new_day
93                 @day += 1
94                 push_popularity
95         end
96
97         def initialize
98                 @stars = {}
99                 @commoners = {}
100                 @popularity = []
101                 reset
102         end
103
104         def reset
105                 @day = 0
106                 @popularity.clear
107                 push_popularity
108         end
109
110         def stars ; @stars.keys ; end
111         def commoners ; @commoners.keys ; end
112
113         # Get :day's [default: today] popularity for :stars [default: all stars].
114         # Returns:
115         #   a hash star => number of commoners that voted for it that day
116         def popularity(o={})
117                 pop = @popularity[o[:day] || today]
118                 sel = {}
119                 [o[:stars] || stars].flatten.each do |s|
120                         sel[s] = pop[s].length
121                 end
122                 return sel
123         end
124
125         # an Array of [star, popularity] ordered by popularity
126         def grade(o={})
127                 popularity(o).to_a.sort { |s1, s2| s2.last <=> s1.last }
128         end
129
130         def vote(o = {})
131                 pop_org = popularity(:day => yesterday)
132                 # to allow giving stars with no popularity a chance,
133                 # we allow a :mul and :add option to the voting session.
134                 mul = o[:mul] || 1
135                 add = o[:add] || 0
136                 tot = 0
137                 pop = {}
138                 pop_org.each do |star, p|
139                         pop[star] = p*mul + add
140                         tot += pop[star]
141                 end
142                 @commoners.each do |name, commoner|
143                         star = commoner.popular_vote(pop, tot)
144                         @popularity[today][star] << name
145                 end
146         end
147
148 end
149
150 pop = Popularity.new
151
152 5.times do |i| pop.add_star(i) end
153 100.times do |i| pop.add_commoner(i) end
154
155 puts pop.grade.map { |s, p| "#{s} (#{p})" }.join(', ')
156
157 puts
158 1000.times do
159         pop.new_day
160         pop.vote
161         puts pop.grade.map { |s, p| "#{s} (#{p})" }.join(', ')
162 end
163
164 puts
165 pop.reset
166 1000.times do
167         pop.new_day
168         pop.vote(:add => 1)
169         puts pop.grade.map { |s, p| "#{s} (#{p})" }.join(', ')
170 end
171
172 puts
173 pop.reset
174 1000.times do
175         pop.new_day
176         pop.vote(:mul => 10, :add => 1)
177         puts pop.grade.map { |s, p| "#{s} (#{p})" }.join(', ')
178 end
179