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 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
- 3. Export a tournament entry YAML file
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
- 4. Create entries. You can use the included buggy GUI (see below),
85
+ 6. Create entries. You can use the included buggy GUI (see below),
72
86
  or edit YAML files by hand.
73
87
 
74
- 5. Import the entry YAML files into the pool
88
+ 7. Import the entry YAML files into the pool
75
89
 
76
90
  pool entry --add=path/to/entry.yml
77
91
 
78
- 6. As games progress, update the tournament.yml file, again using the GUI or
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
- 7. Run reports
98
+ 9. Run reports
85
99
 
86
100
  pool report [final_four|entry|region|leader|score]
87
101
 
88
- 8. After about 22 teams are left, run a possibility report. This report will
89
- run through all the remaining ways the tournament can come out and
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
- pool report possibility
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.0.0'
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
@@ -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
@@ -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
- def self.test(num_picks = 10)
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
- #pool.possibility_report
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
- sep= "--------------+----------------+----------------------------"
313
- puts " Championship | Champion | Winners"
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
- first = rankings[0][0]
318
- second = rankings[1][0]
319
- last = rankings[-1][0]
320
- puts "%14s|%16s|%s" % [poss.winners[5].map{|t| t.short_name}.join("-"),
321
- poss.champion.name, " 1: #{first.name} (#{rankings[0][1]})"]
322
- puts "%14s|%16s|%s" % ['', '',
323
- " 2: #{second.name} (#{rankings[1][1]})"]
324
- puts "%14s|%16s|%s" % ['', '',
325
- "LAST: #{last.name} (#{rankings[-1][1]})"]
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 | %d" %
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.0.0
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-26 00:00:00 -07:00
12
+ date: 2008-03-30 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency