tournament 2.3.0 → 2.4.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 CHANGED
@@ -1,3 +1,6 @@
1
+ == 2.4.0 / 2009-03-23
2
+ * Hook up possibility report.
3
+
1
4
  == 2.3.0 / 2009-03-20
2
5
  * Add ability for admin to send emails to participants. Needs work.
3
6
 
data/Manifest.txt CHANGED
@@ -10,6 +10,7 @@ lib/tournament.rb
10
10
  lib/tournament/bracket.rb
11
11
  lib/tournament/entry.rb
12
12
  lib/tournament/pool.rb
13
+ lib/tournament/possibility.rb
13
14
  lib/tournament/scoring_strategy.rb
14
15
  lib/tournament/team.rb
15
16
  lib/tournament/webgui_installer.rb
@@ -73,6 +74,7 @@ webgui/app/views/layouts/print.html.erb
73
74
  webgui/app/views/layouts/report.html.erb
74
75
  webgui/app/views/pool/index.html.erb
75
76
  webgui/app/views/reports/_leader.html.erb
77
+ webgui/app/views/reports/_possibility.html.erb
76
78
  webgui/app/views/reports/_report.html.erb
77
79
  webgui/app/views/reports/show.html.erb
78
80
  webgui/app/views/sessions/new.html.erb
data/Rakefile CHANGED
@@ -20,7 +20,7 @@ PROJ.authors = 'Douglas A. Seifert'
20
20
  PROJ.email = 'doug+rubyforge@dseifert.net'
21
21
  PROJ.url = 'http://www.dseifert.net/code/tournament'
22
22
  PROJ.rubyforge.name = 'tournament'
23
- PROJ.version = '2.3.0'
23
+ PROJ.version = '2.4.0'
24
24
  PROJ.group_id = 5863
25
25
 
26
26
  PROJ.spec.opts << '--color'
@@ -588,14 +588,20 @@ class Tournament::Pool
588
588
  nil
589
589
  end
590
590
 
591
- # Runs through every possible outcome of the tournament and calculates
592
- # each entry's chance to win as a percentage of the possible outcomes
593
- # the entry would win if the tournament came out that way.
594
- def possibility_report(out = $stdout)
595
- $stdout.sync = true
596
- if @entries.size == 0
597
- out << "There are no entries in the pool." << "\n"
598
- return
591
+ # Runs through every possible outcome of the tournament and
592
+ # calculates each entry's chance to win as a percentage of the
593
+ # possible outcomes the entry would win if the tournment came
594
+ # out that way. If a block is provided, periodically reports
595
+ # progress by calling the block and passing it the percent complete
596
+ # and time remaining in seconds as arguments.
597
+ # Returns array of structs responding to times_champ, max_score,
598
+ # min_rank, entry and champs methods. The entry method returns the
599
+ # Tournament::Entry object and the champs method returns a hash
600
+ # keyed on team name and whose values are the number of times that
601
+ # team could win that would make the entry come in on top.
602
+ def possibility_stats
603
+ stats = @entries.map do |e|
604
+ Tournament::Possibility.new(e)
599
605
  end
600
606
  max_possible_score = @entries.map{|p| 0}
601
607
  min_ranking = @entries.map{|p| @entries.size + 1}
@@ -604,19 +610,20 @@ class Tournament::Pool
604
610
  count = 0
605
611
  old_percentage = -1
606
612
  old_remaining = 1_000_000_000_000
607
- out << "Checking #{self.tournament_entry.picks.number_of_outcomes} possible outcomes" << "\n"
608
613
  start = Time.now.to_f
609
614
  self.tournament_entry.picks.each_possible_bracket do |poss|
610
615
  poss_scores = @entries.map{|p| p.picks.score_against(poss, self.scoring_strategy)}
611
616
  sort_scores = poss_scores.sort.reverse
612
617
  @entries.each_with_index do |entry, i|
613
618
  score = poss_scores[i]
614
- max_possible_score[i] = score if score > max_possible_score[i]
619
+ stat = stats[i]
620
+ stat.max_score = score if score > stat.max_score
615
621
  rank = sort_scores.index(score) + 1
616
- min_ranking[i] = rank if rank < min_ranking[i]
617
- times_winner[i] += 1 if rank == 1
622
+ stat.min_rank = rank if rank < stat.min_rank
623
+ stat.times_champ += 1 if rank == 1
618
624
  if rank == 1
619
- player_champions[i][poss.champion] += 1
625
+ stat.champs[poss.champion.name] ||= 0
626
+ stat.champs[poss.champion.name] += 1
620
627
  end
621
628
  end
622
629
  count += 1
@@ -627,42 +634,55 @@ class Tournament::Pool
627
634
  if (percentage.to_i != old_percentage) || (remaining < old_remaining)
628
635
  old_remaining = remaining
629
636
  old_percentage = percentage.to_i
630
- hashes = '#' * (percentage.to_i/5) + '>'
631
- out << "\rCalculating: %3d%% +#{hashes.ljust(20, '-')}+ %5d seconds remaining" % [percentage.to_i, remaining]
637
+ if block_given?
638
+ yield(percentage.to_i, remaining)
639
+ end
632
640
  end
633
641
  end
642
+ stats.sort!
643
+ return stats
644
+ end
645
+
646
+ # Runs through every possible outcome of the tournament and calculates
647
+ # each entry's chance to win as a percentage of the possible outcomes
648
+ # the entry would win if the tournament came out that way. Generates
649
+ # an ASCII report of the results.
650
+ def possibility_report(out = $stdout)
651
+ $stdout.sync = true
652
+ if @entries.size == 0
653
+ out << "There are no entries in the pool." << "\n"
654
+ return
655
+ end
656
+ out << "Checking #{self.tournament_entry.picks.number_of_outcomes} possible outcomes" << "\n"
657
+ stats = possibility_stats do |percentage, remaining|
658
+ hashes = '#' * (percentage.to_i/5) + '>'
659
+ out << "\rCalculating: %3d%% +#{hashes.ljust(20, '-')}+ %5d seconds remaining" % [percentage.to_i, remaining]
660
+ end
634
661
  out << "\n"
635
- #puts "\n Max Scores: #{max_possible_score.inspect}"
636
- #puts "Highest Place: #{min_ranking.inspect}"
637
- #puts " Times Winner: #{times_winner.inspect}"
638
- sort_array = []
639
- times_winner.each_with_index { |n, i| sort_array << [n, i, min_ranking[i], max_possible_score[i], player_champions[i]] }
640
- sort_array = sort_array.sort_by {|arr| arr[0] == 0 ? (arr[2] == 0 ? -arr[3] : arr[2]) : -arr[0]}
641
- #puts "SORT: #{sort_array.inspect}"
662
+ #puts "SORT: #{stats.inspect}"
642
663
  out << " Entry | Win Chance | Highest Place | Curr Score | Max Score | Tie Break " << "\n"
643
664
  out << "--------------------+------------+---------------+------------+-----------+------------" << "\n"
644
- sort_array.each do |arr|
645
- chance = arr[0].to_f * 100.0 / self.tournament_entry.picks.number_of_outcomes
665
+ stats.each do |stat|
666
+ chance = stat.times_champ.to_f * 100.0 / self.tournament_entry.picks.number_of_outcomes
646
667
  out << "%19s | %10.2f | %13d | %10d | %9d | %7d " %
647
- [@entries[arr[1]].name, chance, min_ranking[arr[1]], @entries[arr[1]].picks.score_against(self.tournament_entry.picks, self.scoring_strategy), max_possible_score[arr[1]], @entries[arr[1]].tie_breaker] << "\n"
668
+ [stat.entry.name, chance, stat.min_rank, stat.entry.picks.score_against(self.tournament_entry.picks, self.scoring_strategy), stat.max_score, stat.entry.tie_breaker] << "\n"
648
669
  end
649
670
  out << "Possible Champions For Win" << "\n"
650
671
  out << " Entry | Champion | Ocurrences | Chance " << "\n"
651
672
  out << "--------------------+-----------------+---------------+---------" << "\n"
652
- sort_array.each do |arr|
653
- next if arr[4].size == 0
654
- arr[4].sort_by{|k,v| -v}.each_with_index do |harr, idx|
673
+ stats.each do |stat|
674
+ next if stat.champs.size == 0
675
+ stat.champs.sort_by{|k,v| -v}.each_with_index do |harr, idx|
655
676
  team = harr[0]
656
677
  occurences = harr[1]
657
678
  if idx == 0
658
- out << "%19s | %15s | %13d | %8.2f " % [@entries[arr[1]].name, team.name, occurences, occurences.to_f * 100.0 / arr[0]] << "\n"
679
+ out << "%19s | %15s | %13d | %8.2f " % [stat.entry.name, team, occurences, occurences.to_f * 100.0 / stat.times_champ] << "\n"
659
680
  else
660
- out << "%19s | %15s | %13d | %8.2f " % ['', team.name, occurences, occurences.to_f * 100.0 / arr[0]] << "\n"
681
+ out << "%19s | %15s | %13d | %8.2f " % ['', team, occurences, occurences.to_f * 100.0 / stat.times_champ] << "\n"
661
682
  end
662
683
  end
663
684
  out << "--------------------+-----------------+---------------+---------" << "\n"
664
685
  end
665
686
  nil
666
687
  end
667
-
668
688
  end
@@ -0,0 +1,19 @@
1
+ # Holds information about an Tournament::Entry's possibilities for
2
+ # remaining games in a tournament.
3
+ class Tournament::Possibility
4
+ include Comparable
5
+ attr_accessor :times_champ, :max_score, :min_rank
6
+ attr_reader :champs, :entry
7
+ def initialize(entry)
8
+ @times_champ = 0
9
+ @max_score = 0
10
+ @min_rank = 1_000_000_000
11
+ @champs = {}
12
+ @entry = entry
13
+ end
14
+ def <=>(other)
15
+ (other.times_champ <=> self.times_champ).nonzero? ||
16
+ (self.min_rank <=> other.min_rank).nonzero? ||
17
+ other.max_score <=> self.max_score
18
+ end
19
+ end
@@ -31,9 +31,10 @@ class AdminController < ApplicationController
31
31
  end
32
32
 
33
33
  def recap
34
+ @pool = Pool.find(params[:id])
34
35
  if request.post?
35
36
  begin
36
- UserMailer.deliver_recap(User.find(:all) - [current_user], params[:subject], params[:content], root_path(:only_path => false))
37
+ UserMailer.deliver_recap(@pool.entrants, params[:subject], params[:content], root_path(:only_path => false))
37
38
  flash[:notice] = "Email was delivered."
38
39
  rescue Exception => e
39
40
  flash[:error] = "Email could not be delivered: #{e}"
@@ -1,21 +1,25 @@
1
1
  class EntryController < ApplicationController
2
2
  layout 'bracket'
3
3
  before_filter :login_required
4
- before_filter :check_access, :only => [:show, :edit, :print, :pdf]
4
+ before_filter :resolve_entry, :only => [:edit, :show, :print, :pdf]
5
+ before_filter :check_access, :only => [:edit]
5
6
  before_filter :pool_taking_edits, :only => [:edit]
6
7
  include SavesPicks
7
8
  include PdfHelper
8
9
 
9
- def check_access
10
+ def resolve_entry
10
11
  # Resolve the entry
11
12
  @entry = params[:id] ? Entry.find(params[:id]) : Entry.new({:user_id => current_user.id, :pool_id => params[:pool_id]})
13
+ end
12
14
 
15
+ def check_access
13
16
  # Admin user
14
17
  return true if current_user.has_role?(:admin)
15
18
 
16
19
  # Check if entry being viewed belongs to current user
17
20
  if current_user != @entry.user
18
- flash[:info] = "You don't have access to that entry."
21
+ flash[:info] = "You can't make edits to that entry. This has been reported to the pool administrator."
22
+ logger.warn("User #{current_user.login} tried to edit entry #{@entry.id}")
19
23
  redirect_to root_path
20
24
  return false
21
25
  end
@@ -62,6 +66,7 @@ class EntryController < ApplicationController
62
66
  end
63
67
 
64
68
  def edit
69
+ @pool = @entry.pool.pool
65
70
  if params[:reset] == 'Reset Picks'
66
71
  @entry.reset
67
72
  if @entry.new_record?
@@ -1,7 +1,61 @@
1
1
  class ReportsController < ApplicationController
2
+ STATS_DATAFILE = File.expand_path(File.join(RAILS_ROOT, 'db', 'stats.yml')) unless defined?(STATS_DATAFILE)
2
3
  layout 'report'
3
4
  def show
4
5
  @pool = Pool.find(params[:id])
6
+ if params[:report] == 'possibility'
7
+ possibility
8
+ end
9
+ end
10
+
11
+ def possibility
12
+ if !File.exist?(STATS_DATAFILE)
13
+ @message = "The statistics data has not yet been generated. Please try again later or send an email to #{ADMIN_EMAIL}."
14
+ @stats = []
15
+ else
16
+ @stats = YAML.load_file(STATS_DATAFILE)
17
+ end
18
+ end
19
+
20
+ def gen_possibility
21
+ @pool = Pool.find(params[:id])
22
+ pool = @pool.pool
23
+ reporter = Proc.new do |response, output|
24
+ output.write "Starting to generate possibility statistics for #{pool.tournament_entry.picks.number_of_outcomes} possible outcomes...<br/>\n"
25
+ output.flush
26
+ stats_thread = Thread.new do
27
+ begin
28
+ Thread.current[:stats] = pool.possibility_stats do |percentage, remaining|
29
+ Thread.current[:percentage] = percentage
30
+ Thread.current[:remaining] = remaining
31
+ end
32
+ rescue Exception => e
33
+ Thread.current[:error] = e
34
+ Thread.current[:percentage] = 100
35
+ end
36
+ end
37
+
38
+ while stats_thread[:percentage].nil? || stats_thread[:percentage] < 100
39
+ logger.info " -> #{stats_thread[:percentage] || 'UNK'}% Complete #{stats_thread[:remaining] || 'UNK'}s remaining ... "
40
+ output.write " -> #{stats_thread[:percentage] || 'UNK'}% Complete #{stats_thread[:remaining] || 'UNK'}s remaining ... <br/>"
41
+ output.flush
42
+ sleep 10
43
+ end
44
+
45
+ output.write "Waiting for thread to end ..."
46
+ stats_thread.join
47
+
48
+ if stats_thread[:error]
49
+ output.write "Got error #{stats_thread[:error]}"
50
+ output.write "<br/>"
51
+ output.flush
52
+ else
53
+ data = stats_thread[:stats]
54
+ File.open(STATS_DATAFILE, "w") {|f| f.write YAML.dump(data)}
55
+ output.write "Generated file!"
56
+ end
57
+ end
58
+ render :text => reporter
5
59
  end
6
60
 
7
61
  end
@@ -7,6 +7,7 @@ class Pool < ActiveRecord::Base
7
7
  belongs_to :user
8
8
  has_many :entries
9
9
  has_many :user_entries, :class_name => 'Entry', :conditions => ['user_id != ?', '#{user_id}']
10
+ has_many :users, :through => :user_entries
10
11
  has_many :pending_entries, :class_name => 'Entry', :conditions => ['completed = ? and user_id != ?', false, '#{user_id}']
11
12
  has_one :tournament_entry, :class_name => 'Entry', :conditions => {:user_id => '#{user_id}'}
12
13
  has_many :seedings
@@ -29,6 +30,11 @@ class Pool < ActiveRecord::Base
29
30
  end
30
31
  end
31
32
 
33
+ # entrants: the unique set of users having entries in this pool
34
+ def entrants
35
+ self.users.uniq
36
+ end
37
+
32
38
  # True if the number of teams in the pool is 64
33
39
  def ready?
34
40
  return teams.size == 64
@@ -1,4 +1,5 @@
1
1
  <h1>Send a Recap Email</h1>
2
+ <h3><%=@pool.name%></h3>
2
3
  <form action="<%=url_for :action => 'recap'%>" method="POST">
3
4
  <table border="0">
4
5
  <tr>
@@ -6,22 +6,26 @@
6
6
  <span class="poollisthead"><%= pool.name %></span>
7
7
  <small>
8
8
  <% if current_user && pool.user_id == current_user.id %>
9
- &nbsp;
10
- <%= link_to '[Bracket]', :controller => 'admin', :action => 'bracket', :id => pool.id %>
11
- <%= link_to '[Recap]', :controller => 'admin', :action => 'recap'%>
9
+ <%= link_to '[Tourny Bracket]', :controller => 'admin', :action => 'bracket', :id => pool.tournament_entry.id %>
10
+ <%= link_to '[Recap]', :controller => 'admin', :action => 'recap', :id => pool.id%>
12
11
  <%= link_to '[Edit]', :controller => 'admin', :action => 'pool', :id => pool.id %>
13
12
  <%= link_to '[Entries]', :controller => 'admin', :action => 'entries', :id => pool.id %>
13
+ <% else %>
14
+ <%= link_to '[Tourny Bracket]', :controller => 'entry', :action => 'show', :id => pool.tournament_entry.id %>
14
15
  <% end %>
15
16
  <%= link_to '[Leader Board]', :controller => 'reports', :action => 'show', :id => pool.id, :report => 'leader' %>
16
17
  <%= link_to '[Reports]', :controller => 'reports', :action => 'show', :id => pool.id %>
17
18
  </small>
18
19
  <div class="poollistinfo">
19
20
  <div class="poollistinfodetail">
20
- Starts: <%= pool.starts_at.to_date.to_formatted_s(:long)%>
21
+ <% if pool.starts_at < Time.zone.now %>Started<%else%>Starts<%end%>: <%= pool.starts_at.to_formatted_s(:long)%>
21
22
  Pending Entries: <%= pool.pending_entries.size%>
22
23
  Total Entries: <%= pool.user_entries.size%>
23
24
  </div>
24
25
  <div class="poollistinfodetail">
26
+ Last Update: <%=pool.updated_at.to_formatted_s(:long)%>
27
+ </div>
28
+ <div class="poollistinfodetail">
25
29
  Entry Fee: $<%=pool.fee%>
26
30
  </div>
27
31
  <div class="poollistinfodetail">
@@ -35,6 +35,7 @@
35
35
  </thead>
36
36
  <tbody>
37
37
  <%
38
+ db_entry_ids = @pool.user_entries.inject({}) {|h,e| h[e.name] = e.id; h}
38
39
  pool.entries.sort do |e1, e2|
39
40
  s1 = e1.picks.score_against(pool.tournament_entry.picks, pool.scoring_strategy)
40
41
  s2 = e2.picks.score_against(pool.tournament_entry.picks, pool.scoring_strategy)
@@ -63,7 +64,7 @@
63
64
  <td><%=rank_display%></td>
64
65
  <td><%=total%></td>
65
66
  <td><%=max%></td>
66
- <td><%=entry.name%></td>
67
+ <td><%=link_to entry.name, :controller => 'entry', :action => 'show', :id => db_entry_ids[entry.name]%></td>
67
68
  <td><%=champ.short_name%> <%=pool.tournament_entry.picks.still_alive?(champ) ? 'Y' : 'N'%></td>
68
69
  <td><%=entry.tie_breaker || '-'%></td>
69
70
  <% round_scores.each do |rs| %>
@@ -0,0 +1,45 @@
1
+ <% show_header = defined?(show_header) ? show_header : true %>
2
+ <% if show_header %>
3
+ <h1>Possibility Report</h1>
4
+ <ul>
5
+ <li>Total games played: <%= pool.tournament_entry.picks.games_played %></li>
6
+ <li>Pool Tie Break: <%=pool.tournament_entry.tie_breaker || '-' %></li>
7
+ <li>Number of entries: <%=pool.entries.size%></li>
8
+ <li>Number of remaining outcomes: <%=pool.tournament_entry.picks.number_of_outcomes%></li>
9
+ </ul>
10
+ <% end %>
11
+ <% if @message %>
12
+ <span class="message"><%=@message%></span>
13
+ <% end %>
14
+ <table class="report">
15
+ <thead>
16
+ <tr>
17
+ <td>Entry</td>
18
+ <td>Chance<br/>for 1st</td>
19
+ <td>Highest<br/>Place</td>
20
+ <td>Current<br/>Score</td>
21
+ <td>Max<br/>Score</td>
22
+ <td>Tie<br/>Break</td>
23
+ <td>Champions for 1st</td>
24
+ </tr>
25
+ </thead>
26
+ <tbody>
27
+ <% @stats.each do |stat| -%>
28
+ <tr class="<%=cycle('even', 'odd', :name => 'rtclass')%>">
29
+ <td><%=stat.entry.name%></td>
30
+ <td><%=stat.times_champ * 100.0/pool.tournament_entry.picks.number_of_outcomes%>%</td>
31
+ <td><%=stat.min_rank.ordinal%></td>
32
+ <td><%=stat.entry.bracket.score_against(pool.tournament_entry.picks, pool.scoring_strategy)%></td>
33
+ <td><%=stat.max_score%></td>
34
+ <td><%=stat.entry.tie_breaker%></td>
35
+ <td>
36
+ <% stat.champs.sort_by{|k,v| -v}.each do |team, occurrences| -%>
37
+ <%=team%> <%=occurrences%> (<%="%5.2f" % (occurrences.to_f * 100.0 / stat.times_champ)%>%)
38
+ <br/>
39
+ <% end -%>
40
+ &nbsp;
41
+ </td>
42
+ </tr>
43
+ <% end -%>
44
+ </tbody>
45
+ </table>
@@ -1,5 +1,5 @@
1
- <% if report == 'leader' %>
2
- <%= render :partial => 'leader', :locals => {:pool => pool, :show_header => true}%>
1
+ <% if ['leader', 'possibility'].include?(report) %>
2
+ <%= render :partial => report, :locals => {:pool => pool, :show_header => true}%>
3
3
  <br/>
4
4
  <br/>
5
5
  <% else %>
@@ -4,6 +4,12 @@
4
4
  <li><%= link_to 'Entries', :report => 'entry'%></li>
5
5
  <li><%= link_to 'Score', :report => 'score'%></li>
6
6
  <li><%= link_to 'Leader', :report => 'leader'%></li>
7
+ <% if @pool.pool.tournament_entry.picks.teams_left <= 16 %>
8
+ <li><%= link_to 'Possibility', :report => 'possibility'%></li>
9
+ <% end %>
10
+ <% if @pool.pool.tournament_entry.picks.teams_left <= 4 %>
11
+ <li><%= link_to 'Final Four', :report => 'final_four'%></li>
12
+ <% end %>
7
13
  </ul>
8
14
  <div id="report">
9
15
  <% if params[:report] -%>
@@ -61,27 +61,29 @@ end
61
61
  <div style="float: left">
62
62
  <table border="0">
63
63
  <tr>
64
- <td><%= f.label :name, "Entry Name"%></td><td><%= f.text_field :name %></td>
64
+ <td><%= f.label :name, "Entry Name"%></td><td><% if is_editable%><%= f.text_field :name %><% else %> : <%=@entry.name%><% end %></td>
65
65
  </tr>
66
66
  <tr>
67
- <td><%= f.label :tie_break, 'Tie Breaker'%></td><td><%= f.text_field :tie_break %></td>
67
+ <td><%= f.label :tie_break, 'Tie Breaker'%></td><td><% if is_editable%><%= f.text_field :tie_break %><%else%> : <%=@entry.tie_break || '-'%><%end%></td>
68
68
  </tr>
69
69
  </table>
70
70
  </div>
71
- <div style="float: left">
71
+ <div style="float: left; margin-left: 25px;">
72
+ <% if @entry.name != @pool.tournament_entry.name %>
72
73
  <small>
73
74
  Current Score: <%= @entry.bracket.score_against(@pool.tournament_entry.picks, @pool.scoring_strategy) %> Max. Possible Score: <%= @entry.bracket.maximum_score(@pool.tournament_entry.picks, @pool.scoring_strategy) %>
74
75
  </small>
76
+ <% end %>
75
77
  </div>
76
78
  <input type="hidden" name="pool_id" value="<%=@entry.pool_id%>">
77
79
  <input type="hidden" name="picks" id="picks" value="">
78
- <% if is_editable %>
79
80
  <div style="clear: both">
81
+ <% if is_editable %>
80
82
  <input type="Submit" value="Save Changes">
81
83
  <%= submit_tag 'Cancel Changes', :name => 'cancel', :confirm => 'Are you sure?'%>
82
84
  <%= submit_tag 'Reset Picks', :name => 'reset', :confirm => 'Are you sure? Resetting will clear all picks and start the bracket over from scratch!'%>
83
- </div>
84
85
  <% end -%>
86
+ </div>
85
87
  <% end -%>
86
88
  <TABLE cellspacing=0 class="bracket">
87
89
  <TR>