tournament 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ # Class representing a bracket in a tournament.
2
+ class Tournament::Bracket
3
+ attr_reader :name # The name of the bracket
4
+ attr_reader :teams # The teams in the bracket
5
+ attr_reader :rounds # The number of rounds in the bracket
6
+ attr_reader :winners # The winners of each game in the bracket
7
+ attr_accessor :scoring_strategy # The strategy used to assign points to correct picks
8
+
9
+ # Class representing a scoring strategy where correct picks
10
+ # are worth 2 X the round number
11
+ class BasicScoringStrategy
12
+ def score(pick, winner, loser, round)
13
+ winner != UNKNOWN_TEAM && pick == winner ? round * 2 : 0
14
+ end
15
+ def description
16
+ "Each game is worth 2 times the round number."
17
+ end
18
+ end
19
+
20
+ # Class representing a scoring strategy where correct picks
21
+ # are worth a base amount per round (3, 5, 11, 19, 30 and 40)
22
+ # plus the seed number of the winner.
23
+ class UpsetScoringStrategy
24
+ PER_ROUND = [3, 5, 11, 19, 30, 40]
25
+ def score(pick, winner, loser, round)
26
+ if winner != UNKNOWN_TEAM && pick == winner
27
+ return PER_ROUND[round-1] + winner.seed
28
+ end
29
+ return 0
30
+ end
31
+ def description
32
+ "Games are worth #{PER_ROUND.join(', ')} per round plus the seed number of the winning team."
33
+ end
34
+ end
35
+
36
+ # Returns names of available strategies. The names returned are suitable
37
+ # for use in the strategy_for_name method
38
+ def self.available_strategies
39
+ return ['basic_scoring_strategy', 'upset_scoring_strategy']
40
+ end
41
+
42
+ # Returns an instantiated strategy class for the named strategy.
43
+ def self.strategy_for_name(name)
44
+ clazz = Tournament::Bracket.const_get(name.capitalize.gsub(/_([a-zA-Z])/) {|m| $1.upcase})
45
+ return clazz.new
46
+ end
47
+
48
+ UNKNOWN_TEAM = :unk unless defined?(UNKNOWN_TEAM)
49
+
50
+ # Creates a new bracket with the given teams
51
+ def initialize(teams = nil)
52
+ @teams = teams || [:t1, :t2, :t3, :t4, :t5, :t6, :t7, :t8, :t9, :t10, :t11, :t12, :t13, :t14, :t15, :t16]
53
+ @rounds = (Math.log(@teams.size)/Math.log(2)).to_i
54
+ @winners = [@teams] + (1..@rounds).map do |r|
55
+ [UNKNOWN_TEAM] * games_in_round(r)
56
+ end
57
+ @scoring_strategy = BasicScoringStrategy.new
58
+ end
59
+
60
+ # Returns true if the provided team has not lost
61
+ def still_alive?(team)
62
+ team_index = @winners[0].index(team)
63
+ game = team_index/2
64
+ round = 1
65
+ #puts "Checking round #{round} game #{game} winner #{@winners[round][game].inspect} team #{team.short_name}"
66
+ while @winners[round][game] == team && round < self.rounds
67
+ round += 1
68
+ game /= 2
69
+ #puts "Checking round #{round} game #{game} winner #{@winners[round][game].inspect} team #{team.short_name}"
70
+ end
71
+ return [UNKNOWN_TEAM, team].include?(@winners[round][game])
72
+ end
73
+
74
+ # Returns the number of games that have been decided in the bracket
75
+ def games_played
76
+ @winners[1..-1].inject(0) { |sum, arr| sum += arr.inject(0) {|sum2, t| sum2 += (t != UNKNOWN_TEAM ? 1 : 0) } }
77
+ end
78
+
79
+ # For each possible outcome remaining in the pool, generates a bracket representing
80
+ # that outcome and yields it to the caller's block. This can take a very long time
81
+ # with more than about 22 teams left.
82
+ def each_possible_bracket
83
+ puts "WARNING: This is likely going to take a very long time ... " if teams_left > 21
84
+ each_possibility do |possibility|
85
+ yield(bracket_for(possibility))
86
+ end
87
+ end
88
+
89
+ # Returns the number of rounds that have been completed
90
+ def number_rounds_complete
91
+ round = 0
92
+ while round < self.rounds
93
+ break if @winners[round+1].any? {|t| t == UNKNOWN_TEAM}
94
+ round += 1
95
+ end
96
+ return round
97
+ end
98
+
99
+ # Returns the number of teams left in the bracket.
100
+ def teams_left
101
+ return 1 + @winners.inject(0) { |memo, arr| arr.inject(memo) {|memo, team| memo += (team == UNKNOWN_TEAM ? 1 : 0)} }
102
+ end
103
+
104
+ # Returns the number of possible outcomes for the bracket
105
+ def number_of_outcomes
106
+ @number_of_outcomes ||= (2 ** (self.teams_left)) / 2
107
+ end
108
+
109
+ # Iterates over each possiblity by representing the possibility as a binary number and
110
+ # yielding each number to the caller's block. The binary number is formed
111
+ # by assuming each game is a bit. If the first team in the matchup wins, the
112
+ # bit is set to 0. If the second team in the matchup wins, the bit is set to
113
+ # 1. repeat for each round and the entire bracket result can be represented.
114
+ # As an example, consider a 8 team bracket:
115
+ #
116
+ # Round 0: t1 t2 t3 t4 t5 t6 t7 t8 Bits
117
+ # 1: t1 t4 t6 t7 0 1 1 0
118
+ # 2: t4 t6 1 0
119
+ # 3: t6 1
120
+ #
121
+ # final binary number: 0110101
122
+ #
123
+ # If no games have been played, we can represent each possibility
124
+ # by every possible 7 bit binary number.
125
+ def each_possibility
126
+ # bit masks of games that have been played
127
+ # played_mask is for any game where a winner has been determined
128
+ # first is a mask where the first of the matched teams won
129
+ # second is a mask where the second of the matched teams won
130
+ shift = 0
131
+ round = @rounds
132
+ played_mask, winners, left_mask = @winners[1..-1].reverse.inject([0,0,0]) do |masks, round_winners|
133
+ game = games_in_round(round)
134
+ round_winners.reverse.each do |game_winner|
135
+ #puts "checking matchup of round #{round} game #{game} winner #{game_winner} matchup #{matchup(round,game)}"
136
+ val = 1 << shift
137
+ if UNKNOWN_TEAM != game_winner
138
+ # played mask
139
+ masks[0] = masks[0] | val
140
+ # winners mask
141
+ if matchup(round,game).index(game_winner) == 1
142
+ masks[1] = masks[1] | val
143
+ end
144
+ else
145
+ # games left mask
146
+ masks[2] = masks[2] | val
147
+ end
148
+ shift += 1
149
+ game -= 1
150
+ end
151
+ round -= 1
152
+ masks
153
+ end
154
+ #puts "played mask: #{Tournament::Bracket.jbin(played_mask, teams.size - 1)}"
155
+ #puts " left mask: #{Tournament::Bracket.jbin(left_mask, teams.size - 1)} #{left_mask}"
156
+ #puts " winners: #{Tournament::Bracket.jbin(winners, teams.size - 1)}"
157
+
158
+ # for the games left mask, figure out which bits are 1 and what
159
+ # their index is. If left mask is 1001, the shifts array would be
160
+ # [0, 3]. If left mask is 1111, the shifts array would be
161
+ # [0, 1, 2, 3]
162
+ count = 0
163
+ shifts = []
164
+ Tournament::Bracket.jbin(left_mask, teams.size - 1).reverse.split('').each do |c|
165
+ if c == '1'
166
+ shifts << count
167
+ end
168
+ count += 1
169
+ end
170
+
171
+ #puts " shifts: #{shifts.inspect}"
172
+
173
+ # Figure out the number of possibilities. This is simply
174
+ # 2 ** shifts.size
175
+ num_possibilities = 2 ** shifts.size
176
+ #num_possibilities = 0
177
+ #shifts.size.times { |n| num_possibilities |= (1 << n) }
178
+
179
+ puts "Checking #{num_possibilities} (#{number_of_outcomes}) possible outcomes."
180
+ possibility = num_possibilities - 1
181
+ while possibility >= 0
182
+ #puts " possibility: #{Tournament::Bracket.jbin(possibility, teams.size - 1)}"
183
+ real_poss = 0
184
+ shifts.each_with_index do |s, i|
185
+ real_poss |= (((possibility & (1 << i)) > 0 ? 1 : 0) << s)
186
+ end
187
+ #puts " real_poss: #{Tournament::Bracket.jbin(real_poss, teams.size - 1)}"
188
+ real_possibility = winners | real_poss
189
+ #puts " possibility: #{Tournament::Bracket.jbin(real_possibility, teams.size - 1)}"
190
+ yield(real_possibility)
191
+ possibility -= 1
192
+ end
193
+ end
194
+
195
+ # Given a binary possibility number, compute the bracket
196
+ # that would result.
197
+ def bracket_for(possibility)
198
+ pick_bracket = Tournament::Bracket.new(self.teams)
199
+ pick_bracket.scoring_strategy = self.scoring_strategy
200
+ round = 1
201
+ while round <= pick_bracket.rounds
202
+ gir = pick_bracket.games_in_round(round)
203
+ game = 1
204
+ while game <= gir
205
+ matchup = pick_bracket.matchup(round, game)
206
+ mask = 1 << (gir - game)
207
+ # Shift for round
208
+ mask = mask << (2 ** (pick_bracket.rounds - round) - 1)
209
+ pick = (mask & possibility) > 0 ? 1 : 0
210
+ #puts "round #{round} game #{game} mask #{Tournament::Bracket.jbin(mask)} poss: #{Tournament::Bracket.jbin(possibility)} pick #{pick} winner #{matchup[pick]}"
211
+ pick_bracket.set_winner(round, game, matchup[pick])
212
+ game += 1
213
+ end
214
+ round += 1
215
+ end
216
+ return pick_bracket
217
+ end
218
+
219
+ # Returns a two element array containing the Team's in the
220
+ # matchup for the given round and game
221
+ def matchup(round, game)
222
+ return @winners[round-1][(game-1)*2..(game-1)*2+1]
223
+ end
224
+
225
+ # Returns true if the given team was the winner of the
226
+ # round and game
227
+ def pick_correct(round, game, team)
228
+ return team != UNKNOWN_TEAM && team == winner(round, game)
229
+ end
230
+
231
+ # Returns the number of games in the given round
232
+ def games_in_round(round)
233
+ return @teams.size / 2 ** round
234
+ end
235
+
236
+ # Returns the winner of the given round and game
237
+ def winner(round, game)
238
+ return @winners[round][game-1]
239
+ end
240
+
241
+ # Returns a two element array whose first element is the winner
242
+ # and the second element is the loser of the given round and game
243
+ def winner_and_loser(round, game)
244
+ winner = winner(round,game)
245
+ if UNKNOWN_TEAM == winner
246
+ return [UNKNOWN_TEAM, UNKNOWN_TEAM]
247
+ end
248
+ matchup = matchup(round, game)
249
+ if matchup[0] == winner
250
+ return matchup
251
+ else
252
+ return matchup.reverse
253
+ end
254
+ end
255
+
256
+ # Sets the winner of the given round and game to the provided team
257
+ def set_winner(round, game, team)
258
+ if UNKNOWN_TEAM == team || matchup(round, game).include?(team)
259
+ @winners[round][game-1] = team
260
+ else
261
+ raise "Round #{round}, Game #{game} matchup does not include team #{team.inspect}"
262
+ end
263
+ end
264
+
265
+ # Pretty print.
266
+ def inspect
267
+ str = ""
268
+ 1.upto(rounds) do |r| str << "round #{r}: games: #{games_in_round(r)}: matchups: #{(1..games_in_round(r)).map{|g| matchup(r,g)}.inspect}\n" end
269
+ str << "Champion: #{champion.inspect}"
270
+ return str
271
+ end
272
+
273
+ # Returns the champion of this bracket
274
+ def champion
275
+ return @winners[@rounds][0]
276
+ end
277
+
278
+ # Compute the maximum possible score if all remaining picks in this
279
+ # bracket turn out to be correct.
280
+ def maximum_score(other_bracket)
281
+ score = 0
282
+ round = 1
283
+ while round <= self.rounds
284
+ games_in_round = self.games_in_round(round)
285
+ game = 1
286
+ while game <= games_in_round
287
+ winner, loser = other_bracket.winner_and_loser(round, game)
288
+ pick = self.winner(round, game)
289
+ winner = pick if winner == UNKNOWN_TEAM && other_bracket.still_alive?(pick)
290
+ score += other_bracket.scoring_strategy.score(pick, winner, loser, round)
291
+ game += 1
292
+ end
293
+ round += 1
294
+ end
295
+ return score
296
+ end
297
+
298
+ # Computes the total score of this bracket using other_bracket
299
+ # as the guide
300
+ def score_against(other_bracket)
301
+ score = 0
302
+ round = 1
303
+ while round <= self.rounds
304
+ games_in_round = self.games_in_round(round)
305
+ game = 1
306
+ while game <= games_in_round
307
+ winner, loser = other_bracket.winner_and_loser(round, game)
308
+ score += other_bracket.scoring_strategy.score(self.winner(round, game), winner, loser, round)
309
+ #puts "round #{round} game #{game} winner #{winner} loser #{loser} pick #{self.winner(round,game)}"
310
+ game += 1
311
+ end
312
+ round += 1
313
+ end
314
+ return score
315
+ end
316
+
317
+ # Compute the score for a particular round against the other_bracket
318
+ # Returns an array of two element arrays, one for each game in the
319
+ # round. The first element of the subarray is the score and the
320
+ # second element is the team that was picked. If the winner of the
321
+ # game is unknown (because it has not been played), the score element
322
+ # will be nil.
323
+ def scores_for_round(round, other_bracket)
324
+ games_in_round = self.games_in_round(round)
325
+ return (1..games_in_round).to_a.map do |g|
326
+ winner, loser = other_bracket.winner_and_loser(round, g)
327
+ pick = self.winner(round, g)
328
+ score = nil
329
+ if winner != UNKNOWN_TEAM || !other_bracket.still_alive?(pick)
330
+ score = other_bracket.scoring_strategy.score(pick, winner, loser, round)
331
+ end
332
+ [score, pick]
333
+ end
334
+ end
335
+
336
+ # Generates a bracket for the provided teams with a random winner
337
+ # for each game.
338
+ def self.random_bracket(teams = nil)
339
+ b = Tournament::Bracket.new(teams)
340
+ 1.upto(b.rounds) do |r|
341
+ 1.upto(b.games_in_round(r)) { |g| b.set_winner(r, g, b.matchup(r, g)[rand(2)]) }
342
+ end
343
+ return b
344
+ end
345
+
346
+ private
347
+
348
+ def self.jbin(num, size = 8)
349
+ return num.to_s(2).rjust(size, '0')
350
+ end
351
+
352
+ end
@@ -0,0 +1,13 @@
1
+ # Class representing an entry in a pool.
2
+ class Tournament::Entry
3
+ attr_accessor :name # Name of the entry
4
+ attr_accessor :picks # The entry picks as a Tournament::Bracket object
5
+ attr_accessor :tie_breaker # The tie breaker object
6
+
7
+ # Create a new entry
8
+ def initialize(name = nil, picks = nil, tie_breaker = 100)
9
+ @name = name
10
+ @picks = picks
11
+ @tie_breaker = tie_breaker
12
+ end
13
+ end