tournament 1.0.0 → 1.1.0
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 +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
|