tournament 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +9 -0
- data/README.txt +34 -14
- data/Rakefile +1 -1
- data/bin/pool +47 -0
- data/lib/tournament/bracket.rb +1 -0
- data/lib/tournament/pool.rb +162 -18
- metadata +2 -2
data/History.txt
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
== 1.1.0 / 2008-03-29
|
2
|
+
|
3
|
+
* Add ability to configure entry fee and payout amounts
|
4
|
+
* Change final four report to use the entry fee and payout amount configuration
|
5
|
+
* Fix bug with cached @number_of_outcomes in Tournament::Pool class.
|
6
|
+
* Add to the possibility report a listing of each entry that could place first
|
7
|
+
that shows frequency of which team would have to be champion for
|
8
|
+
the entry to place first.
|
9
|
+
|
1
10
|
== 1.0.0 / 2008-03-26
|
2
11
|
|
3
12
|
* Change the pool command line script usage to make it simpler (I hope)
|
data/README.txt
CHANGED
@@ -61,40 +61,57 @@ The pool manager would use this program as follows:
|
|
61
61
|
As mentioned above, unless overridden by using the --save-file
|
62
62
|
option, the pool will save itself to the file 'pool.yml'
|
63
63
|
|
64
|
-
|
64
|
+
4. Set the entry fee and payout amounts
|
65
|
+
|
66
|
+
pool fee 10
|
67
|
+
pool payout 1 80
|
68
|
+
pool payout 2 20
|
69
|
+
pool payout last 10 -C
|
70
|
+
|
71
|
+
The above commands say that each entry fee is 10 units (this is all
|
72
|
+
for fun, not profit, right?) and that the 1st place finisher would
|
73
|
+
receive 80% of the total payout, the 2nd place finisher would
|
74
|
+
receive 20% of the total payout and the last place finisher would
|
75
|
+
receive 10 units back (would get her entry fee back). No error
|
76
|
+
checking is done with this. FIXME: Add error checking.
|
77
|
+
|
78
|
+
5. Export a tournament entry YAML file
|
65
79
|
|
66
80
|
pool dump
|
67
81
|
|
68
82
|
This will save the tournament entry file as tournament.yml unless
|
69
83
|
the --entry option is used to override it.
|
70
84
|
|
71
|
-
|
85
|
+
6. Create entries. You can use the included buggy GUI (see below),
|
72
86
|
or edit YAML files by hand.
|
73
87
|
|
74
|
-
|
88
|
+
7. Import the entry YAML files into the pool
|
75
89
|
|
76
90
|
pool entry --add=path/to/entry.yml
|
77
91
|
|
78
|
-
|
92
|
+
8. As games progress, update the tournament.yml file, again using the GUI or
|
79
93
|
editing the YAML file by hand. Then update the pool with the new
|
80
94
|
pool YAML file
|
81
95
|
|
82
96
|
pool update
|
83
97
|
|
84
|
-
|
98
|
+
9. Run reports
|
85
99
|
|
86
100
|
pool report [final_four|entry|region|leader|score]
|
87
101
|
|
88
|
-
|
89
|
-
|
90
|
-
calculate the chance to win for each player. The chance to win
|
91
|
-
is defined as the percentage of possibilities that lead to that player
|
92
|
-
coming out on top in the pool. With more than about 22 teams left
|
93
|
-
(YMMV), this report could take months to run. FIXME (Investigate
|
94
|
-
possibly using EC2 or something to spread the load around, or
|
95
|
-
otherwise optimize the possibility checking algorithm)
|
102
|
+
The final four report can only be run once the final four teams have
|
103
|
+
been determined.
|
96
104
|
|
97
|
-
|
105
|
+
10. After about 22 teams are left, run a possibility report. This report will
|
106
|
+
run through all the remaining ways the tournament can come out and
|
107
|
+
calculate the chance to win for each player. The chance to win
|
108
|
+
is defined as the percentage of possibilities that lead to that player
|
109
|
+
coming out on top in the pool. With more than about 22 teams left
|
110
|
+
(YMMV), this report could take months to run. FIXME (Investigate
|
111
|
+
possibly using EC2 or something to spread the load around, or
|
112
|
+
otherwise optimize the possibility checking algorithm)
|
113
|
+
|
114
|
+
pool report possibility
|
98
115
|
|
99
116
|
== SHOES GUI:
|
100
117
|
|
@@ -110,6 +127,9 @@ The GUI also may be used for keeping the NCAA tournament entry YAML file
|
|
110
127
|
up to date:
|
111
128
|
|
112
129
|
picker tournament.yml
|
130
|
+
|
131
|
+
The GUI works as long as you don't try to go back and change games
|
132
|
+
from played to unknown outcome.
|
113
133
|
|
114
134
|
== REQUIREMENTS:
|
115
135
|
|
data/Rakefile
CHANGED
@@ -16,7 +16,7 @@ PROJ.authors = 'Douglas A. Seifert'
|
|
16
16
|
PROJ.email = 'doug+rubyforge@dseifert.net'
|
17
17
|
PROJ.url = 'http://www.dseifert.net/code/tournament'
|
18
18
|
PROJ.rubyforge_name = 'tournament'
|
19
|
-
PROJ.version = '1.
|
19
|
+
PROJ.version = '1.1.0'
|
20
20
|
PROJ.group_id = 5863
|
21
21
|
|
22
22
|
PROJ.spec_opts << '--color'
|
data/bin/pool
CHANGED
@@ -74,6 +74,53 @@ Main do
|
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
77
|
+
mode('fee') do
|
78
|
+
argument('amount') do
|
79
|
+
required
|
80
|
+
cast :integer
|
81
|
+
arity 1
|
82
|
+
description "The fee charged per entry."
|
83
|
+
end
|
84
|
+
def run
|
85
|
+
load_pool
|
86
|
+
@pool.entry_fee = params['amount'].value
|
87
|
+
save_pool
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
mode('payout') do
|
92
|
+
option('constant-amount', 'C') do
|
93
|
+
optional
|
94
|
+
arity 1
|
95
|
+
cast :boolean
|
96
|
+
description "Specify if the payout is a constant amount rather than a percentage."
|
97
|
+
end
|
98
|
+
argument('rank') do
|
99
|
+
required
|
100
|
+
arity 1
|
101
|
+
description "The rank associated with the payout, such as 1, 2, 3 or last"
|
102
|
+
end
|
103
|
+
argument('amount') do
|
104
|
+
required
|
105
|
+
cast :integer
|
106
|
+
arity 1
|
107
|
+
description "The amount of the payout, either a percentage or constant amount. If a constant amount, the --constant-amount option must be specified."
|
108
|
+
end
|
109
|
+
def run
|
110
|
+
load_pool
|
111
|
+
rank = params['rank'].value
|
112
|
+
if 'last' == rank
|
113
|
+
rank = :last
|
114
|
+
else
|
115
|
+
rank = rank.to_i
|
116
|
+
end
|
117
|
+
amount = params['amount'].value
|
118
|
+
amount = -amount if params['constant-amount'].value
|
119
|
+
@pool.set_payout(rank, amount)
|
120
|
+
save_pool
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
77
124
|
mode('entry') do
|
78
125
|
option('add', 'a') do
|
79
126
|
optional
|
data/lib/tournament/bracket.rb
CHANGED
@@ -257,6 +257,7 @@ class Tournament::Bracket
|
|
257
257
|
def set_winner(round, game, team)
|
258
258
|
if UNKNOWN_TEAM == team || matchup(round, game).include?(team)
|
259
259
|
@winners[round][game-1] = team
|
260
|
+
@number_of_outcomes = nil
|
260
261
|
else
|
261
262
|
raise "Round #{round}, Game #{game} matchup does not include team #{team.inspect}"
|
262
263
|
end
|
data/lib/tournament/pool.rb
CHANGED
@@ -5,11 +5,14 @@
|
|
5
5
|
class Tournament::Pool
|
6
6
|
attr_reader :regions # The regions in the pool
|
7
7
|
attr_reader :entries # Tournament::Entry objects for participants
|
8
|
+
attr_reader :payouts # Hash of payouts by rank
|
9
|
+
attr_accessor :entry_fee # The amount each entry paid to participate
|
8
10
|
|
9
11
|
# Create a new empty pool with no Regions or Entries
|
10
12
|
def initialize
|
11
13
|
@regions = Array.new(4)
|
12
14
|
@entries = []
|
15
|
+
@payouts = {}
|
13
16
|
end
|
14
17
|
|
15
18
|
# add regions to the pool. Champ of region with index = 0 plays
|
@@ -40,6 +43,19 @@ class Tournament::Pool
|
|
40
43
|
end
|
41
44
|
end
|
42
45
|
|
46
|
+
# Set a payout. Takes a rank (or the symbol :last for
|
47
|
+
# last place), along with the payout. The payout may be
|
48
|
+
# a positive integer, in which case, it represents a
|
49
|
+
# percentage of the the total entry fees that particular
|
50
|
+
# rank would receive. The payout may also be a negative
|
51
|
+
# integer, in which case, it represents a constant
|
52
|
+
# payout amount.
|
53
|
+
def set_payout(rank, payout)
|
54
|
+
# FIXME: Add error checking
|
55
|
+
@payouts ||= {}
|
56
|
+
@payouts[rank] = payout
|
57
|
+
end
|
58
|
+
|
43
59
|
# Creates a bracket for the pool by combining all the
|
44
60
|
# regions into one bracket of 64 teams. By default the
|
45
61
|
# bracket uses the basic scoring strategy.
|
@@ -152,9 +168,15 @@ class Tournament::Pool
|
|
152
168
|
)
|
153
169
|
return pool
|
154
170
|
end
|
155
|
-
|
156
|
-
|
171
|
+
|
172
|
+
# Run a test pool with random entries and a random outcome.
|
173
|
+
def self.test(num_picks = 20)
|
157
174
|
pool = ncaa_2008
|
175
|
+
pool.entry_fee = 10
|
176
|
+
pool.set_payout(1, 70)
|
177
|
+
pool.set_payout(2, 20)
|
178
|
+
pool.set_payout(3, 10)
|
179
|
+
pool.set_payout(:last, -10)
|
158
180
|
b = pool.bracket
|
159
181
|
b.scoring_strategy = Tournament::Bracket::UpsetScoringStrategy.new
|
160
182
|
picks = (1..num_picks).map {|n| Tournament::Bracket.random_bracket(b.teams)}
|
@@ -163,17 +185,22 @@ class Tournament::Pool
|
|
163
185
|
16.times { |n| b.set_winner(2,n+1, b.matchup(2, n+1)[rand(2)])}
|
164
186
|
8.times { |n| b.set_winner(3,n+1, b.matchup(3, n+1)[rand(2)])}
|
165
187
|
4.times { |n| b.set_winner(4,n+1, b.matchup(4, n+1)[rand(2)])}
|
166
|
-
2.times { |n| b.set_winner(5,n+1, b.matchup(5, n+1)[rand(2)])}
|
167
|
-
1.times { |n| b.set_winner(6,n+1, b.matchup(6, n+1)[rand(2)])}
|
188
|
+
#2.times { |n| b.set_winner(5,n+1, b.matchup(5, n+1)[rand(2)])}
|
189
|
+
#1.times { |n| b.set_winner(6,n+1, b.matchup(6, n+1)[rand(2)])}
|
168
190
|
picks.each_with_index {|p, idx| pool.add_entry Tournament::Entry.new("picker_#{idx}", p) }
|
169
191
|
picks.each_with_index do |p, idx|
|
170
192
|
puts "Score #{idx+1}: #{p.score_against(b)}"
|
171
193
|
end
|
172
|
-
|
194
|
+
pool.region_report
|
173
195
|
pool.leader_report
|
196
|
+
pool.final_four_report
|
197
|
+
pool.possibility_report
|
174
198
|
pool.entry_report
|
199
|
+
pool.score_report
|
175
200
|
end
|
176
201
|
|
202
|
+
# Generate the leader board report. Shows each entry sorted by current
|
203
|
+
# score and gives a breakdown of score by round.
|
177
204
|
def leader_report
|
178
205
|
puts "Total games played: #{@bracket.games_played}"
|
179
206
|
puts "Number of entries: #{@entries.size}"
|
@@ -198,6 +225,9 @@ class Tournament::Pool
|
|
198
225
|
end
|
199
226
|
end
|
200
227
|
|
228
|
+
# Shows detailed scores per entry. For each pick in each game, shows
|
229
|
+
# either a positive amount if the pick was correct, 0 if the pick was
|
230
|
+
# incorrect, or a '?' if the game has not yet been played.
|
201
231
|
def score_report
|
202
232
|
# Compute scores
|
203
233
|
puts "Total games played: #{@bracket.games_played}"
|
@@ -234,6 +264,7 @@ class Tournament::Pool
|
|
234
264
|
end
|
235
265
|
end
|
236
266
|
|
267
|
+
# Splits str on space chars in chunks of around len size
|
237
268
|
def self.split_line(str, len)
|
238
269
|
new_str = []
|
239
270
|
beg_idx = 0
|
@@ -248,6 +279,7 @@ class Tournament::Pool
|
|
248
279
|
return new_str.reject {|s| s.nil? || s.length == 0}
|
249
280
|
end
|
250
281
|
|
282
|
+
# Shows a report of each entry's picks by round.
|
251
283
|
def entry_report
|
252
284
|
puts "There are #{@entries.size} entries."
|
253
285
|
if @entries.size > 0
|
@@ -281,6 +313,7 @@ class Tournament::Pool
|
|
281
313
|
end
|
282
314
|
end
|
283
315
|
|
316
|
+
# Displays the regions and teams in the region.
|
284
317
|
def region_report
|
285
318
|
puts " Region | Seed | Team "
|
286
319
|
current_idx = -1
|
@@ -297,6 +330,8 @@ class Tournament::Pool
|
|
297
330
|
end
|
298
331
|
end
|
299
332
|
|
333
|
+
# When there are four teams left, for each of the 16 possible outcomes
|
334
|
+
# shows who will win according to the configured payouts.
|
300
335
|
def final_four_report
|
301
336
|
if @entries.size == 0
|
302
337
|
puts "There are no entries in the pool."
|
@@ -307,26 +342,114 @@ class Tournament::Pool
|
|
307
342
|
puts "are exactly four teams left in the tournament."
|
308
343
|
return
|
309
344
|
end
|
345
|
+
total_payout = @entries.size * @entry_fee
|
346
|
+
# Subtract out constant payments
|
347
|
+
total_payout = @payouts.values.inject(total_payout) {|t, amount| t += amount if amount < 0; t}
|
348
|
+
|
349
|
+
payout_keys = @payouts.keys.sort do |a,b|
|
350
|
+
if Symbol === a
|
351
|
+
1
|
352
|
+
elsif Symbol === b
|
353
|
+
-1
|
354
|
+
else
|
355
|
+
a <=> b
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
310
359
|
print "Final Four: #{self.bracket.winners[4][0,2].map{|t| "(#{t.seed}) #{t.name}"}.join(" vs. ")}"
|
311
360
|
puts " #{self.bracket.winners[4][2,2].map{|t| "(#{t.seed}) #{t.name}"}.join(" vs. ")}"
|
312
|
-
|
313
|
-
|
361
|
+
puts "Payouts"
|
362
|
+
payout_keys.each do |key|
|
363
|
+
amount = if @payouts[key] > 0
|
364
|
+
@payouts[key].to_f / 100.0 * total_payout
|
365
|
+
else
|
366
|
+
-@payouts[key]
|
367
|
+
end
|
368
|
+
puts "%4s: $%5.2f" % [key, amount]
|
369
|
+
end
|
370
|
+
sep= "--------------+----------------+-----------------------------------------"
|
371
|
+
puts " | | Winners Tie "
|
372
|
+
puts " Championship | Champion | Rank Score Break Name"
|
314
373
|
puts sep
|
315
374
|
self.bracket.each_possible_bracket do |poss|
|
316
375
|
rankings = @entries.map{|p| [p, p.picks.score_against(poss)] }.sort_by {|arr| -arr[1] }
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
puts "
|
325
|
-
|
376
|
+
finishers = {}
|
377
|
+
@payouts.each do |rank, payout|
|
378
|
+
finishers[rank] = {}
|
379
|
+
finishers[rank][:payout] = payout
|
380
|
+
finishers[rank][:entries] = []
|
381
|
+
finishers[rank][:score] = 0
|
382
|
+
end
|
383
|
+
#puts "Got finishers: #{finishers.inspect}"
|
384
|
+
index = 0
|
385
|
+
rank = 1
|
386
|
+
while index < @entries.size
|
387
|
+
rank_score = rankings[index][1]
|
388
|
+
finishers_key = index < (@entries.size - 1) ? rank : :last
|
389
|
+
finish_hash = finishers[finishers_key]
|
390
|
+
#puts "For rank_score = #{rank_score} finishers key = #{finishers_key.inspect}, hash = #{finish_hash}, index = #{index}"
|
391
|
+
if finish_hash
|
392
|
+
while index < @entries.size && rankings[index][1] == rank_score
|
393
|
+
finish_hash[:entries] << rankings[index][0]
|
394
|
+
finish_hash[:score] = rank_score
|
395
|
+
index += 1
|
396
|
+
end
|
397
|
+
rank += 1
|
398
|
+
next
|
399
|
+
end
|
400
|
+
index += 1
|
401
|
+
rank += 1
|
402
|
+
end
|
403
|
+
|
404
|
+
num_payouts = payout_keys.size
|
405
|
+
|
406
|
+
first_line = true
|
407
|
+
showed_last = false
|
408
|
+
payout_count = 0
|
409
|
+
while payout_count < num_payouts
|
410
|
+
rank = payout_keys[payout_count]
|
411
|
+
finish_hash = finishers[rank]
|
412
|
+
label = finish_hash[:entries].size == 1 ? "#{rank}".upcase : "TIE"
|
413
|
+
finish_hash[:entries].each do |winner|
|
414
|
+
line = if first_line
|
415
|
+
"%14s|%16s| %4s %5d %5d %s" % [
|
416
|
+
poss.winners[5].map{|t| t.short_name}.join("-"),
|
417
|
+
poss.champion.name,
|
418
|
+
label,
|
419
|
+
finish_hash[:score],
|
420
|
+
winner.tie_breaker,
|
421
|
+
winner.name
|
422
|
+
]
|
423
|
+
else
|
424
|
+
"%14s|%16s| %4s %5d %5d %s" % [
|
425
|
+
'',
|
426
|
+
'',
|
427
|
+
label,
|
428
|
+
finish_hash[:score],
|
429
|
+
winner.tie_breaker,
|
430
|
+
winner.name
|
431
|
+
]
|
432
|
+
end
|
433
|
+
puts line
|
434
|
+
first_line = false
|
435
|
+
end
|
436
|
+
payout_count += finish_hash[:entries].size
|
437
|
+
showed_last = (rank == :last)
|
438
|
+
if payout_count >= num_payouts && !showed_last
|
439
|
+
if payout_keys[num_payouts-1] == :last
|
440
|
+
payout_count -= 1
|
441
|
+
showed_last = true
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
326
445
|
puts sep
|
327
446
|
end
|
447
|
+
nil
|
328
448
|
end
|
329
449
|
|
450
|
+
# Runs through every possible outcome of the tournament and calculates
|
451
|
+
# each entry's chance to win as a percentage of the possible outcomes
|
452
|
+
# the entry would win if the tournament came out that way.
|
330
453
|
def possibility_report
|
331
454
|
$stdout.sync = true
|
332
455
|
if @entries.size == 0
|
@@ -336,6 +459,7 @@ class Tournament::Pool
|
|
336
459
|
max_possible_score = @entries.map{|p| 0}
|
337
460
|
min_ranking = @entries.map{|p| @entries.size + 1}
|
338
461
|
times_winner = @entries.map{|p| 0 }
|
462
|
+
player_champions = @entries.map{|p| Hash.new {|h,k| h[k] = 0} }
|
339
463
|
count = 0
|
340
464
|
old_percentage = -1
|
341
465
|
old_remaining = 1_000_000_000_000
|
@@ -350,6 +474,9 @@ class Tournament::Pool
|
|
350
474
|
rank = sort_scores.index(score) + 1
|
351
475
|
min_ranking[i] = rank if rank < min_ranking[i]
|
352
476
|
times_winner[i] += 1 if rank == 1
|
477
|
+
if rank == 1
|
478
|
+
player_champions[i][poss.champion] += 1
|
479
|
+
end
|
353
480
|
end
|
354
481
|
count += 1
|
355
482
|
percentage = (count * 100.0 / self.bracket.number_of_outcomes)
|
@@ -368,16 +495,33 @@ class Tournament::Pool
|
|
368
495
|
#puts "Highest Place: #{min_ranking.inspect}"
|
369
496
|
#puts " Times Winner: #{times_winner.inspect}"
|
370
497
|
sort_array = []
|
371
|
-
times_winner.each_with_index { |n, i| sort_array << [n, i, min_ranking[i], max_possible_score[i]] }
|
498
|
+
times_winner.each_with_index { |n, i| sort_array << [n, i, min_ranking[i], max_possible_score[i], player_champions[i]] }
|
372
499
|
sort_array = sort_array.sort_by {|arr| arr[0] == 0 ? (arr[2] == 0 ? -arr[3] : arr[2]) : -arr[0]}
|
373
500
|
#puts "SORT: #{sort_array.inspect}"
|
374
501
|
puts " Entry | Win Chance | Highest Place | Max Score | Tie Break "
|
375
502
|
puts "--------------------+------------+---------------+-----------+------------"
|
376
503
|
sort_array.each do |arr|
|
377
504
|
chance = arr[0].to_f * 100.0 / self.bracket.number_of_outcomes
|
378
|
-
puts "%19s | %10.2f | %13d | %9d | %
|
505
|
+
puts "%19s | %10.2f | %13d | %9d | %7d " %
|
379
506
|
[@entries[arr[1]].name, chance, min_ranking[arr[1]], max_possible_score[arr[1]], @entries[arr[1]].tie_breaker]
|
380
507
|
end
|
508
|
+
puts "Possible Champions For Win"
|
509
|
+
puts " Entry | Champion | Ocurrences | Chance "
|
510
|
+
puts "--------------------+-----------------+---------------+---------"
|
511
|
+
sort_array.each do |arr|
|
512
|
+
next if arr[4].size == 0
|
513
|
+
arr[4].sort_by{|k,v| -v}.each_with_index do |harr, idx|
|
514
|
+
team = harr[0]
|
515
|
+
occurences = harr[1]
|
516
|
+
if idx == 0
|
517
|
+
puts "%19s | %15s | %13d | %8.2f " % [@entries[arr[1]].name, team.name, occurences, occurences.to_f * 100.0 / arr[0]]
|
518
|
+
else
|
519
|
+
puts "%19s | %15s | %13d | %8.2f " % ['', team.name, occurences, occurences.to_f * 100.0 / arr[0]]
|
520
|
+
end
|
521
|
+
end
|
522
|
+
puts "--------------------+-----------------+---------------+---------"
|
523
|
+
end
|
524
|
+
nil
|
381
525
|
end
|
382
526
|
|
383
527
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tournament
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Douglas A. Seifert
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-03-
|
12
|
+
date: 2008-03-30 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|