icu_tournament 1.3.5 → 1.3.6
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/icu_tournament/tie_break.rb +92 -0
- data/lib/icu_tournament/tournament.rb +25 -49
- data/lib/icu_tournament/tournament_spx.rb +1 -1
- data/lib/icu_tournament/version.rb +1 -1
- data/lib/icu_tournament.rb +1 -1
- data/spec/tie_break_spec.rb +100 -0
- metadata +6 -3
@@ -0,0 +1,92 @@
|
|
1
|
+
module ICU
|
2
|
+
#
|
3
|
+
# This class is used to recognise the names of tie break rules. The class method _identify_
|
4
|
+
# takes a string as it's only argument and returns a new intstance if it recognises a
|
5
|
+
# tie break rule from the string, or _nil_ otherwise. An instance has three read-only methods:
|
6
|
+
# _id_ (a shoet symbolic name), _code_ (a two letter code) and _name_ (the full name).
|
7
|
+
# For example:
|
8
|
+
#
|
9
|
+
# ICU::TieBreak.identify("no such rule") # => nil
|
10
|
+
# tb = ICU::TieBreak.identify("Neustadlt")
|
11
|
+
# tb.id # => :neustadtl
|
12
|
+
# tb.code # => "SB"
|
13
|
+
# tb.name # => "Sonneborn-Berger"
|
14
|
+
#
|
15
|
+
# The method is case insensitive and can cope with extraneous white space and,
|
16
|
+
# to a limited extent, name variations and spelling mistakes:
|
17
|
+
#
|
18
|
+
# ICU::TieBreak.identify("SB").name # => "Sonneborn-Berger"
|
19
|
+
# ICU::TieBreak.identify("NESTADL").name # => "Sonneborn-Berger"
|
20
|
+
# ICU::TieBreak.identify(" wins ").name # => "Number of wins"
|
21
|
+
# ICU::TieBreak.identify(:sum_ratings).name # => "Sum of opponent's ratings"
|
22
|
+
# ICU::TieBreak.identify("median").name # => "Harkness"
|
23
|
+
# ICU::TieBreak.identify("MODIFIED").name # => "Modified median"
|
24
|
+
# ICU::TieBreak.identify("Modified\nMedian").name # => "Modified median"
|
25
|
+
# ICU::TieBreak.identify("\tbuccholts\t").name # => "Buchholz"
|
26
|
+
# ICU::TieBreak.identify("progressive\r\n").name # => "Sum of progressive scores"
|
27
|
+
# ICU::TieBreak.identify("SumOfCumulative").name # => "Sum of progressive scores"
|
28
|
+
#
|
29
|
+
# The full list of supported tie break rules is:
|
30
|
+
#
|
31
|
+
# * Buchholz (:buchholz, "BH"): sum of opponents' scores
|
32
|
+
# * Harkness (:harkness, "HK"): like Buchholz except the highest and lowest opponents' scores are discarded (or two highest and lowest if 9 rounds or more)
|
33
|
+
# * Modified median (:modified_median, "MM"): same as Harkness except only lowest (or highest) score(s) are discarded for players with more (or less) than 50%
|
34
|
+
# * Number of blacks (:blacks, "NB") number of blacks
|
35
|
+
# * Number of wins (:wins, "NW") number of wins
|
36
|
+
# * Player's name (:name, "PN"): alphabetical by name
|
37
|
+
# * Sonneborn-Berger (:neustadtl, "SB"): sum of scores of players defeated plus half sum of scores of players drawn against
|
38
|
+
# * Sum of opponents' ratings (:ratings, "SR"): sum of opponents ratings (FIDE ratings are used in preference to local ratings if available)
|
39
|
+
# * Sum of progressive scores (:progressive, "SP"): sum of running score for each round
|
40
|
+
#
|
41
|
+
# An array of all supported TieBreak instances (ordered by name) is returned by the class method _rules_.
|
42
|
+
#
|
43
|
+
# rules = ICU::TieBreak.rules
|
44
|
+
# rules.size # => 9
|
45
|
+
# rules.first.name # => "Buchholz"
|
46
|
+
#
|
47
|
+
# Note that this class only deals with the recognition of tie break names, not the calculation of tie break scores.
|
48
|
+
# The latter is currently implemented in the ICU::Tournament class.
|
49
|
+
#
|
50
|
+
class TieBreak
|
51
|
+
attr_reader :id, :code, :name
|
52
|
+
private_class_method :new
|
53
|
+
|
54
|
+
RULES =
|
55
|
+
{
|
56
|
+
:blacks => ["NB", "Number of blacks", %r{(number[-_ ]?of[-_ ])?blacks?}],
|
57
|
+
:buchholz => ["BH", "Buchholz", %r{^buc{1,2}h{1,2}olt?[zs]}],
|
58
|
+
:harkness => ["HK", "Harkness", %r{^(harkness?|median)$}],
|
59
|
+
:modified_median => ["MM", "Modified median", %r{^modified([-_ ]?median)?$}],
|
60
|
+
:name => ["PN", "Player's name", %r{^(player('?s)?[-_ ]?)?name$}],
|
61
|
+
:neustadtl => ["SB", "Sonneborn-Berger", %r{^(sonn?eborn[-_ ]?berger|n[eu]{1,2}sta[dtl]{2,3})}],
|
62
|
+
:progressive => ["SP", "Sum of progressive scores", %r{^(sum[-_ ]?(of[-_ ]?)?)?(progressive|cumulative)([-_ ]?scores?)?}],
|
63
|
+
:ratings => ["SR", "Sum of opponent's ratings", %r{(sum[-_ ]?(of[-_ ]?)?)?(opponents'?[-_ ]?)?ratings}],
|
64
|
+
:wins => ["NW", "Number of wins", %r{(number[-_ ]?of[-_ ])?wins?}],
|
65
|
+
}
|
66
|
+
|
67
|
+
# Given a string, return the TieBreak rule it is recognised as, or return nil.
|
68
|
+
def self.identify(str)
|
69
|
+
return nil unless str
|
70
|
+
str = str.to_s.gsub(/\s+/, ' ').strip.downcase
|
71
|
+
return nil if str.length <= 1 || str.length == 3
|
72
|
+
RULES.each_pair { |id, rule| return new(id, rule[0], rule[1]) if str.upcase == rule[0] || str.match(rule[2]) }
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# Return an array of all tie break rules, ordered by name.
|
77
|
+
def self.rules
|
78
|
+
RULES.keys.sort_by{ |id| RULES[id][1] }.inject([]) do |rules, id|
|
79
|
+
rules << new(id, RULES[id][0], RULES[id][1])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# :enddoc:
|
84
|
+
private
|
85
|
+
|
86
|
+
def initialize(id, code, name)
|
87
|
+
@id = id
|
88
|
+
@code = code
|
89
|
+
@name = name
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -105,17 +105,7 @@ module ICU
|
|
105
105
|
# t.tie_breaks = [:buchholz, :neustadtl, :blacks, :wins]
|
106
106
|
# t.tie_breaks = [] # reset to the default
|
107
107
|
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
# * _Buchholz_: sum of opponents' scores
|
111
|
-
# * _Harkness_ (or _median_): like Buchholz except the highest and lowest opponents' scores are discarded (or two highest and lowest if 9 rounds or more)
|
112
|
-
# * _modified_median_: same as Harkness except only lowest (or highest) score(s) are discarded for players with more (or less) than 50%
|
113
|
-
# * _Neustadtl_ (or _Sonneborn-Berger_): sum of scores of players defeated plus half sum of scores of players drawn against
|
114
|
-
# * _progressive_ (or _cumulative_): sum of running score for each round
|
115
|
-
# * _ratings_: sum of opponents ratings (FIDE ratings are used in preference to local ratings if available)
|
116
|
-
# * _blacks_: number of blacks
|
117
|
-
# * _wins_: number of wins
|
118
|
-
# * _name_: alphabetical by name (if _tie_breaks_ is set to an empty array, as it is initially, then this will be used as the back-up tie breaker)
|
108
|
+
# See ICU::TieBreak for the full list of supported tie break methods.
|
119
109
|
#
|
120
110
|
# The return value from _rerank_ is the tournament object itself, to allow chaining, for example:
|
121
111
|
#
|
@@ -226,28 +216,14 @@ module ICU
|
|
226
216
|
@teams.find{ |t| t.matches(name) }
|
227
217
|
end
|
228
218
|
|
229
|
-
#
|
219
|
+
# Canonicalise the names in the tie break array.
|
230
220
|
def tie_breaks=(tie_breaks)
|
231
221
|
raise "argument error - always set tie breaks to an array" unless tie_breaks.class == Array
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
case m
|
237
|
-
when true then 'name'
|
238
|
-
when 'sonneborn_berger' then 'neustadtl'
|
239
|
-
when 'modified_median' then 'modified'
|
240
|
-
when 'median' then 'harkness'
|
241
|
-
when 'cumulative' then 'progressive'
|
242
|
-
else m
|
243
|
-
end
|
222
|
+
@tie_breaks = tie_breaks.map do |str|
|
223
|
+
tb = ICU::TieBreak.identify(str)
|
224
|
+
raise "invalid tie break method '#{str}'" unless tb
|
225
|
+
tb.id
|
244
226
|
end
|
245
|
-
|
246
|
-
# Check they're all valid.
|
247
|
-
tie_breaks.each { |m| raise "invalid tie break method '#{m}'" unless m.to_s.match(/^(blacks|buchholz|harkness|modified|name|neustadtl|progressive|ratings|wins)$/) }
|
248
|
-
|
249
|
-
# Finally set them.
|
250
|
-
@tie_breaks = tie_breaks;
|
251
227
|
end
|
252
228
|
|
253
229
|
# Add a new player to the tournament. Must have a unique player number.
|
@@ -508,7 +484,7 @@ module ICU
|
|
508
484
|
end
|
509
485
|
end
|
510
486
|
|
511
|
-
# Return an array of tie break
|
487
|
+
# Return an array of tie break rules and an array of tie break orders (+1 for asc, -1 for desc).
|
512
488
|
# The first and most important method is always "score", the last and least important is always "name".
|
513
489
|
def tie_break_data
|
514
490
|
|
@@ -516,19 +492,19 @@ module ICU
|
|
516
492
|
methods, order, data = Array.new, Hash.new, Hash.new
|
517
493
|
|
518
494
|
# Score is always the most important.
|
519
|
-
methods <<
|
520
|
-
order[
|
495
|
+
methods << :score
|
496
|
+
order[:score] = -1
|
521
497
|
|
522
498
|
# Add the configured methods.
|
523
499
|
tie_breaks.each do |m|
|
524
500
|
methods << m
|
525
|
-
order[m] = m ==
|
501
|
+
order[m] = m == :name ? 1 : -1
|
526
502
|
end
|
527
503
|
|
528
504
|
# Name is included as the last and least important tie breaker unless it's already been added.
|
529
|
-
unless methods.include?(
|
530
|
-
methods <<
|
531
|
-
order[
|
505
|
+
unless methods.include?(:name)
|
506
|
+
methods << :name
|
507
|
+
order[:name] = 1
|
532
508
|
end
|
533
509
|
|
534
510
|
# We'll need the number of rounds.
|
@@ -537,7 +513,7 @@ module ICU
|
|
537
513
|
# Pre-calculate some scores that are not in themselves tie break scores
|
538
514
|
# but are needed in the calculation of some of the actual tie-break scores.
|
539
515
|
pre_calculated = Array.new
|
540
|
-
pre_calculated <<
|
516
|
+
pre_calculated << :opp_score # sum scores where a non-played games counts 0.5
|
541
517
|
pre_calculated.each do |m|
|
542
518
|
data[m] = Hash.new
|
543
519
|
@player.values.each { |p| data[m][p.num] = tie_break_score(data, m, p, rounds) }
|
@@ -557,20 +533,20 @@ module ICU
|
|
557
533
|
# Return a tie break score for a given player and a given tie break method.
|
558
534
|
def tie_break_score(hash, method, player, rounds)
|
559
535
|
case method
|
560
|
-
when
|
561
|
-
when
|
562
|
-
when
|
563
|
-
when
|
564
|
-
when
|
565
|
-
when
|
566
|
-
when
|
567
|
-
when
|
568
|
-
when
|
569
|
-
scores = player.results.map{ |r| r.opponent ? hash[
|
536
|
+
when :score then player.points
|
537
|
+
when :wins then player.results.inject(0) { |t,r| t + (r.opponent && r.score == 'W' ? 1 : 0) }
|
538
|
+
when :blacks then player.results.inject(0) { |t,r| t + (r.opponent && r.colour == 'B' ? 1 : 0) }
|
539
|
+
when :buchholz then player.results.inject(0.0) { |t,r| t + (r.opponent ? hash[:opp_score][r.opponent] : 0.0) }
|
540
|
+
when :neustadtl then player.results.inject(0.0) { |t,r| t + (r.opponent ? hash[:opp_score][r.opponent] * r.points : 0.0) }
|
541
|
+
when :opp_score then player.results.inject(0.0) { |t,r| t + (r.opponent ? r.points : 0.5) } + (rounds - player.results.size) * 0.5
|
542
|
+
when :progressive then (1..rounds).inject(0.0) { |t,n| r = player.find_result(n); s = r ? r.points : 0.0; t + s * (rounds + 1 - n) }
|
543
|
+
when :ratings then player.results.inject(0) { |t,r| t + (r.opponent && (@player[r.opponent].fide_rating || @player[r.opponent].rating) ? (@player[r.opponent].fide_rating || @player[r.opponent].rating) : 0) }
|
544
|
+
when :harkness, :modified_median
|
545
|
+
scores = player.results.map{ |r| r.opponent ? hash[:opp_score][r.opponent] : 0.0 }.sort
|
570
546
|
1.upto(rounds - player.results.size) { scores << 0.0 }
|
571
547
|
half = rounds / 2.0
|
572
548
|
times = rounds >= 9 ? 2 : 1
|
573
|
-
if method ==
|
549
|
+
if method == :harkness || player.points == half
|
574
550
|
1.upto(times) { scores.shift; scores.pop }
|
575
551
|
else
|
576
552
|
1.upto(times) { scores.send(player.points > half ? :shift : :pop) }
|
@@ -100,7 +100,7 @@ module ICU
|
|
100
100
|
#
|
101
101
|
# You may wish set the tie-break rules before ranking:
|
102
102
|
#
|
103
|
-
# tournament.tie_breaks = [:buchholz,
|
103
|
+
# tournament.tie_breaks = [:buchholz, :neustadtl]
|
104
104
|
# spexport = tournament.rerank.renumber.serialize('SwissPerfect')
|
105
105
|
#
|
106
106
|
# See ICU::Tournament for more about tie-breaks.
|
data/lib/icu_tournament.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require 'icu_name'
|
4
4
|
|
5
5
|
icu_tournament_files = Array.new
|
6
|
-
icu_tournament_files.concat %w{util federation}
|
6
|
+
icu_tournament_files.concat %w{util federation tie_break}
|
7
7
|
icu_tournament_files.concat %w{player result team tournament}
|
8
8
|
icu_tournament_files.concat %w{fcsv krause sp spx}.map{ |f| "tournament_#{f}"}
|
9
9
|
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
module ICU
|
4
|
+
describe TieBreak do
|
5
|
+
context "#identify which rule" do
|
6
|
+
it "should recognize Buchholz" do
|
7
|
+
TieBreak.identify(:buchholz).id.should == :buchholz
|
8
|
+
TieBreak.identify(" BucholtS ").id.should == :buchholz
|
9
|
+
TieBreak.identify(" bh ").id.should == :buchholz
|
10
|
+
TieBreak.identify(" buccholts ").code.should == "BH"
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should recognize Harkness (Median)" do
|
14
|
+
TieBreak.identify(:harkness).id.should == :harkness
|
15
|
+
TieBreak.identify("median").id.should == :harkness
|
16
|
+
TieBreak.identify(" hARKNES ").id.should == :harkness
|
17
|
+
TieBreak.identify("HK").id.should == :harkness
|
18
|
+
TieBreak.identify("MEDIAN").code.should == "HK"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should recognize Modified Median" do
|
22
|
+
TieBreak.identify(:modified).id.should == :modified_median
|
23
|
+
TieBreak.identify(" modified MEDIAN ").id.should == :modified_median
|
24
|
+
TieBreak.identify("MM").code.should == "MM"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should recognize Number of Blacks" do
|
28
|
+
TieBreak.identify(:blacks).id.should == :blacks
|
29
|
+
TieBreak.identify("number\tof\tblacks\n").id.should == :blacks
|
30
|
+
TieBreak.identify("\tnb\t").id.should == :blacks
|
31
|
+
TieBreak.identify("number_blacks").code.should == "NB"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should recognize Number of Wins" do
|
35
|
+
TieBreak.identify(:wins).id.should == :wins
|
36
|
+
TieBreak.identify(" number-of-wins ").id.should == :wins
|
37
|
+
TieBreak.identify("NUMBER WINS\r\n").id.should == :wins
|
38
|
+
TieBreak.identify("nw").code.should == "NW"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should recognize Player's of Name" do
|
42
|
+
TieBreak.identify(:name).id.should == :name
|
43
|
+
TieBreak.identify("Player's Name").id.should == :name
|
44
|
+
TieBreak.identify("players_name").id.should == :name
|
45
|
+
TieBreak.identify("PN").id.should == :name
|
46
|
+
TieBreak.identify("PLAYER-NAME").code.should == "PN"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should recognize Sonneborn-Berger" do
|
50
|
+
TieBreak.identify(:sonneborn_berger).id.should == :neustadtl
|
51
|
+
TieBreak.identify(:neustadtl).id.should == :neustadtl
|
52
|
+
TieBreak.identify(" SONNEBORN\nberger").id.should == :neustadtl
|
53
|
+
TieBreak.identify("\t soneborn_berger \t").id.should == :neustadtl
|
54
|
+
TieBreak.identify("sb").id.should == :neustadtl
|
55
|
+
TieBreak.identify("NESTADL").code.should == "SB"
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should recognize Sum of Progressive Scores" do
|
59
|
+
TieBreak.identify(:progressive).id.should == :progressive
|
60
|
+
TieBreak.identify("CUMULATIVE").id.should == :progressive
|
61
|
+
TieBreak.identify("sum of progressive scores").id.should == :progressive
|
62
|
+
TieBreak.identify("SUM-cumulative_SCORE").id.should == :progressive
|
63
|
+
TieBreak.identify(:cumulative_score).id.should == :progressive
|
64
|
+
TieBreak.identify("SumOfCumulative").id.should == :progressive
|
65
|
+
TieBreak.identify("SP").code.should == "SP"
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should recognize Sum of Opponents' Ratings" do
|
69
|
+
TieBreak.identify(:ratings).id.should == :ratings
|
70
|
+
TieBreak.identify("sum of opponents ratings").id.should == :ratings
|
71
|
+
TieBreak.identify("Opponents' Ratings").id.should == :ratings
|
72
|
+
TieBreak.identify("SR").id.should == :ratings
|
73
|
+
TieBreak.identify("SUMOPPONENTSRATINGS").code.should == "SR"
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should recognize player's name" do
|
77
|
+
TieBreak.identify(:name).id.should == :name
|
78
|
+
TieBreak.identify(" player's NAME ").id.should == :name
|
79
|
+
TieBreak.identify("pn").code.should == "PN"
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should return nil for unrecognized tie breaks" do
|
83
|
+
TieBreak.identify("XYZ").should be_nil
|
84
|
+
TieBreak.identify(nil).should be_nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "return an array of tie break rules" do
|
89
|
+
before(:each) do
|
90
|
+
@rules = TieBreak.rules
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should be an array in a specific order" do
|
94
|
+
@rules.size.should == 9
|
95
|
+
@rules.first.name.should == "Buchholz"
|
96
|
+
@rules.map(&:code).join("|").should == "BH|HK|MM|NB|NW|PN|SB|SR|SP"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 1
|
7
7
|
- 3
|
8
|
-
-
|
9
|
-
version: 1.3.
|
8
|
+
- 6
|
9
|
+
version: 1.3.6
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Mark Orr
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-02-
|
17
|
+
date: 2011-02-27 00:00:00 +00:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -135,6 +135,7 @@ files:
|
|
135
135
|
- lib/icu_tournament/player.rb
|
136
136
|
- lib/icu_tournament/result.rb
|
137
137
|
- lib/icu_tournament/team.rb
|
138
|
+
- lib/icu_tournament/tie_break.rb
|
138
139
|
- lib/icu_tournament/tournament.rb
|
139
140
|
- lib/icu_tournament/tournament_fcsv.rb
|
140
141
|
- lib/icu_tournament/tournament_krause.rb
|
@@ -148,6 +149,7 @@ files:
|
|
148
149
|
- spec/result_spec.rb
|
149
150
|
- spec/spec_helper.rb
|
150
151
|
- spec/team_spec.rb
|
152
|
+
- spec/tie_break_spec.rb
|
151
153
|
- spec/tournament_fcsv_spec.rb
|
152
154
|
- spec/tournament_krause_spec.rb
|
153
155
|
- spec/tournament_sp_spec.rb
|
@@ -194,6 +196,7 @@ test_files:
|
|
194
196
|
- spec/result_spec.rb
|
195
197
|
- spec/spec_helper.rb
|
196
198
|
- spec/team_spec.rb
|
199
|
+
- spec/tie_break_spec.rb
|
197
200
|
- spec/tournament_fcsv_spec.rb
|
198
201
|
- spec/tournament_krause_spec.rb
|
199
202
|
- spec/tournament_sp_spec.rb
|