lib/chess_icu/tournament.rb in sanichi-chess_icu-0.5.2 vs lib/chess_icu/tournament.rb in sanichi-chess_icu-0.5.3

- old
+ new

@@ -54,23 +54,38 @@ t.validate!(:rerank => true) then there are additional side effects of validating a tournament: -* the players will be ranked if no players have any rank +* the players will be ranked if there is no existing ranking * the players will be reranked if the existing ranking is inconsistent Ranking is consistent if either no players have any rank or if all players have a rank and no player is ranked higher than another player with more points. -The players in a tournament, whose reference numbers can be any set of unique integers (including zero and negative numbers), -can be renumbered in order of rank or name. After renumbering the new player numbers will start at 1 and go up to the number -of players. +The default tie break method used to rank players on the same score is alphabetical (by last name then first name). +Other methods can be specified via the _rerank_ option. Instead of setting the option to _true_, alternatives are +the following (both symbols or strings will work): - t.renumber!(:name) # renumber by name - t.renumber!(:rank) # renumber by rank - t.renumber! # same - rank is the default +* _sum_of_scores_: sum of opponents' scores +* _name_: this is the default and the same as setting the option to true +Since _validate_ and _invalid_ only rerank a tournament with absent or inconsistent ranking, to force +a particular kind of ranking of a tournament which is already ranked, use the _rerank_ method. This method +takes one argument, the tie break method and it works the same as the _rerank_ option to _validate_. For example: + + t.rerank(:sum_of_scores) # rerank using sum of scores as tie break + t.rerank(:name) # rerank using player names as tie break + t.rerank # same as _name_, which is the default + +The players in a tournament, whose reference numbers can be any set of unique integers (including zero and +negative numbers), can be renumbered in order of rank or name. After renumbering the new player numbers will +start at 1 and go up to the number of players. + + t.renumber(:name) # renumber by name + t.renumber(:rank) # renumber by rank + t.renumber # same - rank is the default + A side effect of renumbering by rank is that if the tournament started without any player rankings or with inconsitent rankings, it will be reranked (i.e. the method _rerank_ will be called). =end @@ -254,24 +269,24 @@ reverse.rateable = false unless reverse_rateable @player[result.opponent].add_result(reverse) end end - # Rerank the tournament by score, resolving ties using name. - def rerank - @player.values.map{ |p| [p, p.points] }.sort do |a,b| - d = b[1] <=> a[1] - d = a[0].last_name <=> b[0].last_name if d == 0 - d = a[0].first_name <=> b[0].first_name if d == 0 - d - end.each_with_index do |v,i| - v[0].rank = i + 1 + # Rerank the tournament by score first and if necessary using a configurable tie breaker method. + def rerank(tie_break_method = :name) + points, tie_break_scores, tie_break_order = rerank_data(tie_break_method) + sortable = @player.values.map { |p| [p, points[p.num], tie_break_scores[p.num]] } + sortable.sort do |a,b| + diff = b[1] <=> a[1] + diff == 0 ? (b[2] <=> a[2]) * tie_break_order : diff + end.each_with_index do |s,i| + s[0].rank = i + 1 end end - - # Renumber the players according to a given criterion. Return self. - def renumber!(criterion = :rank) + + # Renumber the players according to a given criterion. + def renumber(criterion = :rank) map = Hash.new # Decide how to rank. if criterion == :name @player.values.sort_by{ |p| p.name }.each_with_index{ |p, i| map[p.num] = i + 1 } @@ -279,18 +294,16 @@ begin check_ranks rescue rerank end @player.values.each{ |p| map[p.num] = p.rank} end # Apply ranking. - @teams.each{ |t| t.renumber!(map) } + @teams.each{ |t| t.renumber(map) } @player = @player.values.inject({}) do |hash, player| - player.renumber!(map) + player.renumber(map) hash[player.num] = player hash end - - self end # Is a tournament invalid? Either returns false (if it's valid) or an error message. def invalid(options={}) begin @@ -302,11 +315,11 @@ end # Raise an exception if a tournament is not valid. # Covers all the ways a tournament can be invalid not already enforced by the setters. def validate!(options={}) - begin check_ranks rescue rerank end if options[:rerank] + begin check_ranks rescue rerank(options[:rerank]) end if options[:rerank] check_players check_rounds check_dates check_teams check_ranks(:allow_none => true) @@ -398,8 +411,35 @@ p1 = by_rank[i-1] p2 = by_rank[i] raise "player #{p1.num} with #{p1.points} points is ranked above player #{p2.num} with #{p2.points} points" if p1.points < p2.points end end + end + + # Return a hash of total points, a hash of tie break scores and a tie break + # order (+1 for ascending, -1 for descending) depending on the tie break method. + def rerank_data(tie_break_method) + points = Hash.new + @player.values.each do |p| + points[p.num] = p.points + end + tie_break_scores = Hash.new + tie_break_method = tie_break_method.to_sym if tie_break_method.class == String + @player.values.each do |p| + if tie_break_method == :name || tie_break_method == true + tie_break_scores[p.num] = p.name + else + tie_break_scores[p.num] = 0.0 + if (tie_break_method == :sum_of_scores) + p.results.each do |r| + tie_break_scores[p.num]+= points[r.opponent] if r.opponent + end + else + raise "invalid tie break method '#{tie_break_method}'" + end + end + end + tie_break_order = tie_break_method == :name ? -1 : 1 + [points, tie_break_scores, tie_break_order] end end end