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.
- data/History.txt +4 -0
- data/Manifest.txt +29 -0
- data/README.txt +113 -0
- data/Rakefile +27 -0
- data/bin/benchmark_pool +12 -0
- data/bin/gui.rb +236 -0
- data/bin/picker +12 -0
- data/bin/pool +118 -0
- data/lib/tournament/bracket.rb +352 -0
- data/lib/tournament/entry.rb +13 -0
- data/lib/tournament/pool.rb +341 -0
- data/lib/tournament/team.rb +19 -0
- data/lib/tournament.rb +56 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/tournament_spec.rb +8 -0
- data/static/shoes-icon.png +0 -0
- data/tasks/ann.rake +76 -0
- data/tasks/annotations.rake +22 -0
- data/tasks/bones.rake +40 -0
- data/tasks/doc.rake +48 -0
- data/tasks/gem.rake +116 -0
- data/tasks/manifest.rake +49 -0
- data/tasks/post_load.rake +32 -0
- data/tasks/rubyforge.rake +57 -0
- data/tasks/setup.rb +227 -0
- data/tasks/spec.rake +56 -0
- data/tasks/svn.rake +44 -0
- data/tasks/test.rake +38 -0
- data/test/test_tournament.rb +0 -0
- metadata +97 -0
@@ -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
|