tournament 2.3.0 → 2.4.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,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>