Represents a NCAA tournament pool. Contains 4 regions of 16 teams each. Champions of Region 1 and Region 2 and champions of Region 3 and Region 4 play each other in the final four.
- add_entry
- add_entry_yaml
- add_region
- bracket
- bracket=
- entry_report
- final_four_report
- leader_report
- ncaa_2008
- new
- possibility_report
- region_report
- remove_by_name
- score_report
- set_payout
- split_line
- test
| [R] | entries | |
| [RW] | entry_fee | |
| [R] | payouts | |
| [R] | regions |
Creates a Pool object for the 2008 NCAA tournament
[ show source ]
# File lib/tournament/pool.rb, line 83
83: def self.ncaa_2008
84: pool = Tournament::Pool.new
85: pool.add_region("East",
86: [
87: Tournament::Team.new('North Carolina', 'UNC', 1),
88: Tournament::Team.new('Mt. St. Mary\'s', 'MSM', 16),
89: Tournament::Team.new('Indiana', 'Ind', 8),
90: Tournament::Team.new('Arkansas', 'Ark', 9),
91: Tournament::Team.new('Notre Dame', 'ND', 5),
92: Tournament::Team.new('George Mason', 'GM', 12),
93: Tournament::Team.new('Washington St.', 'WSt', 4),
94: Tournament::Team.new('Winthrop', 'Win', 13),
95: Tournament::Team.new('Oklahoma', 'Okl', 6),
96: Tournament::Team.new('St. Joseph\'s', 'StJ', 11),
97: Tournament::Team.new('Louisville', 'Lou', 3),
98: Tournament::Team.new('Boise St.', 'BSt', 14),
99: Tournament::Team.new('Butler', 'But', 7),
100: Tournament::Team.new('South Alabama', 'SAl', 10),
101: Tournament::Team.new('Tennessee', 'Ten', 2),
102: Tournament::Team.new('American', 'Am', 15)
103: ],
104: 0
105: )
106: pool.add_region("Midwest",
107: [
108: Tournament::Team.new('Kansas', 'Kan', 1),
109: Tournament::Team.new('Portland St.', 'PSt', 16),
110: Tournament::Team.new('UNLV', 'ULV', 8),
111: Tournament::Team.new('Kent St.', 'KSt', 9),
112: Tournament::Team.new('Clemson', 'Clm', 5),
113: Tournament::Team.new('Villanova', 'Vil', 12),
114: Tournament::Team.new('Vanderbilt', 'Van', 4),
115: Tournament::Team.new('Siena', 'Sie', 13),
116: Tournament::Team.new('USC', 'USC', 6),
117: Tournament::Team.new('Kansas St.', 'KSU', 11),
118: Tournament::Team.new('Wisconsin', 'Wis', 3),
119: Tournament::Team.new('CSU Fullerton', 'CSF', 14),
120: Tournament::Team.new('Gonzaga', 'Gon', 7),
121: Tournament::Team.new('Davidson', 'Dav', 10),
122: Tournament::Team.new('Georgetown', 'GT', 2),
123: Tournament::Team.new('UMBC', 'UBC', 15)
124: ],
125: 1
126: )
127: pool.add_region("South",
128: [
129: Tournament::Team.new('Memphis', 'Mem', 1),
130: Tournament::Team.new('TX Arlington', 'TxA', 16),
131: Tournament::Team.new('Mississippi St.', 'MiS', 8),
132: Tournament::Team.new('Oregon', 'Ore', 9),
133: Tournament::Team.new('Michigan St.', 'MSU', 5),
134: Tournament::Team.new('Temple', 'Tem', 12),
135: Tournament::Team.new('Pittsburgh', 'Pit', 4),
136: Tournament::Team.new('Oral Roberts', 'ORo', 13),
137: Tournament::Team.new('Marquette', 'Mar', 6),
138: Tournament::Team.new('Kentucky', 'Ken', 11),
139: Tournament::Team.new('Stanford', 'Sta', 3),
140: Tournament::Team.new('Cornell', 'Cor', 14),
141: Tournament::Team.new('Miami (FL)', 'Mia', 7),
142: Tournament::Team.new('St. Mary\'s', 'StM', 10),
143: Tournament::Team.new('Texas', 'Tex', 2),
144: Tournament::Team.new('Austin Peay', 'APe', 15)
145: ],
146: 2
147: )
148: pool.add_region("West",
149: [
150: Tournament::Team.new('UCLA', 'ULA', 1),
151: Tournament::Team.new('Mis. Valley St', 'MVS', 16),
152: Tournament::Team.new('BYU', 'BYU', 8),
153: Tournament::Team.new('Texas A&M', 'A&M', 9),
154: Tournament::Team.new('Drake', 'Dra', 5),
155: Tournament::Team.new('W. Kentucky', 'WKy', 12),
156: Tournament::Team.new('Connecticut', 'Con', 4),
157: Tournament::Team.new('San Diego', 'SD', 13),
158: Tournament::Team.new('Purdue', 'Pur', 6),
159: Tournament::Team.new('Baylor', 'Bay', 11),
160: Tournament::Team.new('Xavier', 'Xav', 3),
161: Tournament::Team.new('Georgia', 'UG', 14),
162: Tournament::Team.new('West Virginia', 'WVa', 7),
163: Tournament::Team.new('Arizona', 'UA', 10),
164: Tournament::Team.new('Duke', 'Duk', 2),
165: Tournament::Team.new('Belmont', 'Bel', 15)
166: ],
167: 3
168: )
169: return pool
170: end
Create a new empty pool with no Regions or Entries
[ show source ]
# File lib/tournament/pool.rb, line 12
12: def initialize
13: @regions = Array.new(4)
14: @entries = []
15: @payouts = {}
16: end
Splits str on space chars in chunks of around len size
[ show source ]
# File lib/tournament/pool.rb, line 268
268: def self.split_line(str, len)
269: new_str = []
270: beg_idx = 0
271: end_idx = len - 1
272: while end_idx < str.length
273: end_idx += 1 while end_idx < (str.length - 1) && str[end_idx].chr != ' '
274: new_str << str[beg_idx,(end_idx-beg_idx+1)].strip
275: beg_idx = end_idx + 1
276: end_idx += len
277: end
278: new_str << str[beg_idx,str.length-1]
279: return new_str.reject {|s| s.nil? || s.length == 0}
280: end
Run a test pool with random entries and a random outcome.
[ show source ]
# File lib/tournament/pool.rb, line 173
173: def self.test(num_picks = 20)
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)
180: b = pool.bracket
181: b.scoring_strategy = Tournament::Bracket::UpsetScoringStrategy.new
182: picks = (1..num_picks).map {|n| Tournament::Bracket.random_bracket(b.teams)}
183: # Play out the bracket
184: 32.times { |n| b.set_winner(1,n+1, b.matchup(1, n+1)[rand(2)])}
185: 16.times { |n| b.set_winner(2,n+1, b.matchup(2, n+1)[rand(2)])}
186: 8.times { |n| b.set_winner(3,n+1, b.matchup(3, n+1)[rand(2)])}
187: 4.times { |n| b.set_winner(4,n+1, b.matchup(4, 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)])}
190: picks.each_with_index {|p, idx| pool.add_entry Tournament::Entry.new("picker_#{idx}", p) }
191: picks.each_with_index do |p, idx|
192: puts "Score #{idx+1}: #{p.score_against(b)}"
193: end
194: pool.region_report
195: pool.leader_report
196: pool.final_four_report
197: pool.possibility_report
198: pool.entry_report
199: pool.score_report
200: end
Add an Tournament::Entry object to the pool
[ show source ]
# File lib/tournament/pool.rb, line 28
28: def add_entry(entry)
29: @entries << entry
30: end
Add an Tournament::Entry object to the pool after reading the Tournament::Entry from the provided YAML file.
[ show source ]
# File lib/tournament/pool.rb, line 34
34: def add_entry_yaml(yaml)
35: @entries << YAML::load_file(yaml)
36: end
add regions to the pool. Champ of region with index = 0 plays region with index = 1 and index == 2 plays index == 3.
[ show source ]
# File lib/tournament/pool.rb, line 20
20: def add_region(name, teams, index)
21: @regions[index] = {
22: :name => name,
23: :teams => teams
24: }
25: end
Creates a bracket for the pool by combining all the regions into one bracket of 64 teams. By default the bracket uses the basic scoring strategy.
[ show source ]
# File lib/tournament/pool.rb, line 62
62: def bracket
63: unless @bracket
64: if @regions.compact.size != 4
65: raise "Not all regions have been set."
66: end
67: all_teams = @regions.map do |region|
68: region[:teams]
69: end
70: all_teams = all_teams.flatten
71: @bracket = Tournament::Bracket.new(all_teams)
72: end
73: return @bracket
74: end
[ show source ]
# File lib/tournament/pool.rb, line 78
78: def bracket=(new_bracket)
79: @bracket = new_bracket
80: end
Shows a report of each entry‘s picks by round.
[ show source ]
# File lib/tournament/pool.rb, line 283
283: def entry_report
284: puts "There are #{@entries.size} entries."
285: if @entries.size > 0
286: puts "".center(15) + "|" + "First Round".center(128)
287: puts "Name".center(15) + "|" + "Sweet 16".center(64) + "|" + "Elite 8".center(32) +
288: "|" + "Final 4".center(16) + "|" + "Final 2".center(8) + "|" + "Champion".center(15) +
289: "|" + "Tie Break"
290: puts ("-" * 15) + "+" + ("-" * 64) + "+" + ("-" * 32) +
291: "+" + ("-" * 16) + "+" + ("-" * 8) + "+" + ("-" * 15) +
292: "+" + ("-" * 10)
293: output = Proc.new do |name, bracket, tie_breaker|
294: first_round = bracket.winners[1].map {|t| "%s" % (t.short_name rescue 'Unk')}.join('-')
295: sweet_16 = bracket.winners[2].map {|t| "%s" % (t.short_name rescue 'Unk')}.join('-')
296: elite_8 = bracket.winners[3].map {|t| "%s" % (t.short_name rescue 'Unk')}.join('-')
297: final_4 = bracket.winners[4].map {|t| "%s" % (t.short_name rescue 'Unk')}.join('-')
298: final_2 = bracket.winners[5].map {|t| "%s" % (t.short_name rescue 'Unk')}.join('-')
299: champ = bracket.champion.name rescue 'Unk'
300: puts " |%128s" % first_round
301: puts "%15s|%64s|%32s|%16s|%8s|%15s|%s" %
302: [name, sweet_16, elite_8, final_4, final_2, champ, tie_breaker.to_s]
303: puts ("-" * 15) + "+" + ("-" * 64) + "+" + ("-" * 32) +
304: "+" + ("-" * 16) + "+" + ("-" * 8) + "+" + ("-" * 15) +
305: "+" + ("-" * 10)
306: end
307:
308: output.call('Tournament', bracket, '-')
309:
310: @entries.sort_by{|e| e.name}.each do |entry|
311: output.call(entry.name, entry.picks, entry.tie_breaker)
312: end
313: end
314: end
When there are four teams left, for each of the 16 possible outcomes shows who will win according to the configured payouts.
[ show source ]
# File lib/tournament/pool.rb, line 335
335: def final_four_report
336: if @entries.size == 0
337: puts "There are no entries in the pool."
338: return
339: end
340: if self.bracket.teams_left != 4
341: puts "The final four report should only be run when there"
342: puts "are exactly four teams left in the tournament."
343: return
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:
359: print "Final Four: #{self.bracket.winners[4][0,2].map{|t| "(#{t.seed}) #{t.name}"}.join(" vs. ")}"
360: puts " #{self.bracket.winners[4][2,2].map{|t| "(#{t.seed}) #{t.name}"}.join(" vs. ")}"
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"
373: puts sep
374: self.bracket.each_possible_bracket do |poss|
375: rankings = @entries.map{|p| [p, p.picks.score_against(poss)] }.sort_by {|arr| -arr[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
445: puts sep
446: end
447: nil
448: end
Generate the leader board report. Shows each entry sorted by current score and gives a breakdown of score by round.
[ show source ]
# File lib/tournament/pool.rb, line 204
204: def leader_report
205: puts "Total games played: #{@bracket.games_played}"
206: puts "Number of entries: #{@entries.size}"
207: if @entries.size > 0
208: puts " Curr| Max | |Champ| Round Scores"
209: puts "Score|Score| Name |Live?|" + (1..bracket.rounds).to_a.map{|r| "%3d" % r}.join(" ")
210: sep ="-----+-----+---------------+-----+" + ("-" * 4 * bracket.rounds)
211: puts sep
212: @entries.sort_by {|e| -e.picks.score_against(bracket)}.each do |entry|
213: total = entry.picks.score_against(bracket)
214: max = entry.picks.maximum_score(bracket)
215: champ = entry.picks.champion
216: round_scores = []
217: 1.upto(bracket.rounds) do |round|
218: scores = entry.picks.scores_for_round(round, bracket)
219: round_scores << scores.inject(0) {|sum, arr| sum += (arr[0] ? arr[0] : 0)}
220: end
221: puts "%5d|%5d|%15s|%3s %1s|%s" % [total, max, entry.name,
222: champ.short_name,(bracket.still_alive?(champ) ? 'Y' : 'N'), round_scores.map {|s| "%3d" % s}.join(" ")]
223: end
224: puts sep
225: end
226: end
Runs through every possible outcome of the tournament and calculates each entry‘s chance to win as a percentage of the possible outcomes the entry would win if the tournament came out that way.
[ show source ]
# File lib/tournament/pool.rb, line 453
453: def possibility_report
454: $stdout.sync = true
455: if @entries.size == 0
456: puts "There are no entries in the pool."
457: return
458: end
459: max_possible_score = @entries.map{|p| 0}
460: min_ranking = @entries.map{|p| @entries.size + 1}
461: times_winner = @entries.map{|p| 0 }
462: player_champions = @entries.map{|p| Hash.new {|h,k| h[k] = 0} }
463: count = 0
464: old_percentage = -1
465: old_remaining = 1_000_000_000_000
466: puts "Checking #{self.bracket.number_of_outcomes} possible outcomes"
467: start = Time.now.to_f
468: self.bracket.each_possible_bracket do |poss|
469: poss_scores = @entries.map{|p| p.picks.score_against(poss)}
470: sort_scores = poss_scores.sort.reverse
471: @entries.each_with_index do |entry, i|
472: score = poss_scores[i]
473: max_possible_score[i] = score if score > max_possible_score[i]
474: rank = sort_scores.index(score) + 1
475: min_ranking[i] = rank if rank < min_ranking[i]
476: times_winner[i] += 1 if rank == 1
477: if rank == 1
478: player_champions[i][poss.champion] += 1
479: end
480: end
481: count += 1
482: percentage = (count * 100.0 / self.bracket.number_of_outcomes)
483: elapsed = Time.now.to_f - start
484: spp = elapsed / count
485: remaining = ((self.bracket.number_of_outcomes - count) * spp).to_i
486: if (percentage.to_i != old_percentage) || (remaining < old_remaining)
487: old_remaining = remaining
488: old_percentage = percentage.to_i
489: hashes = '#' * (percentage.to_i/5) + '>'
490: print "\rCalculating: %3d%% +#{hashes.ljust(20, '-')}+ %5d seconds remaining" % [percentage.to_i, remaining]
491: end
492: end
493: puts "\n"
494: #puts "\n Max Scores: #{max_possible_score.inspect}"
495: #puts "Highest Place: #{min_ranking.inspect}"
496: #puts " Times Winner: #{times_winner.inspect}"
497: sort_array = []
498: times_winner.each_with_index { |n, i| sort_array << [n, i, min_ranking[i], max_possible_score[i], player_champions[i]] }
499: sort_array = sort_array.sort_by {|arr| arr[0] == 0 ? (arr[2] == 0 ? -arr[3] : arr[2]) : -arr[0]}
500: #puts "SORT: #{sort_array.inspect}"
501: puts " Entry | Win Chance | Highest Place | Max Score | Tie Break "
502: puts "--------------------+------------+---------------+-----------+------------"
503: sort_array.each do |arr|
504: chance = arr[0].to_f * 100.0 / self.bracket.number_of_outcomes
505: puts "%19s | %10.2f | %13d | %9d | %7d " %
506: [@entries[arr[1]].name, chance, min_ranking[arr[1]], max_possible_score[arr[1]], @entries[arr[1]].tie_breaker]
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
525: end
Displays the regions and teams in the region.
[ show source ]
# File lib/tournament/pool.rb, line 317
317: def region_report
318: puts " Region | Seed | Team "
319: current_idx = -1
320: @regions.each_with_index do |region, idx|
321: region[:teams].each do |team|
322: region_name = ''
323: if idx != current_idx
324: region_name = region[:name]
325: current_idx = idx
326: puts "--------+------+-------------------------"
327: end
328: puts "%8s|%6d|%25s" % [region_name, team.seed, "#{team.name} (#{team.short_name})"]
329: end
330: end
331: end
Remove an entry by name from the pool
[ show source ]
# File lib/tournament/pool.rb, line 39
39: def remove_by_name(name)
40: entry = @entries.find {|e| e.name == name}
41: if !entry.nil?
42: @entries.delete(entry)
43: end
44: end
Shows detailed scores per entry. For each pick in each game, shows either a positive amount if the pick was correct, 0 if the pick was incorrect, or a ’?’ if the game has not yet been played.
[ show source ]
# File lib/tournament/pool.rb, line 231
231: def score_report
232: # Compute scores
233: puts "Total games played: #{@bracket.games_played}"
234: puts "Number of entries: #{@entries.size}"
235: sep = "-----+---------------+----------------------------------------------------------------------------------"
236: if @entries.size > 0
237: puts "Total| Name | Round Scores"
238: puts sep
239: fmt1 = "%5d|%15s|%d: %3d %s"
240: fmt2 = " | |%d: %3d %s"
241: fmt3 = " | | %s"
242: @entries.sort_by {|e| -e.picks.score_against(bracket)}.each do |entry|
243: total = entry.picks.score_against(bracket)
244: 1.upto(bracket.rounds) do |round|
245: scores = entry.picks.scores_for_round(round, bracket)
246: round_total = scores.inject(0) {|sum, arr| sum += (arr[0] ? arr[0] : 0)}
247: scores_str = scores.map{|arr| "#{arr[1].short_name}=#{arr[0] ? arr[0] : '?'}"}.join(" ")
248: if [1,2].include?(round)
249: scores_str_arr = Tournament::Pool.split_line(scores_str, 70)
250: if round == 1
251: puts fmt1 % [total, entry.name, round, round_total, scores_str_arr[0]]
252: else
253: puts fmt2 % [round, round_total, scores_str_arr[0]]
254: end
255: scores_str_arr[1..-1].each do |scores_str|
256: puts fmt3 % scores_str
257: end
258: else
259: puts fmt2 % [round, round_total, scores_str]
260: end
261: end
262: puts sep
263: end
264: end
265: end
Set a payout. Takes a rank (or the symbol :last for last place), along with the payout. The payout may be a positive integer, in which case, it represents a percentage of the the total entry fees that particular rank would receive. The payout may also be a negative integer, in which case, it represents a constant payout amount.
[ show source ]
# File lib/tournament/pool.rb, line 53
53: def set_payout(rank, payout)
54: # FIXME: Add error checking
55: @payouts ||= {}
56: @payouts[rank] = payout
57: end