icu_tournament 1.1.2 → 1.2.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/README.rdoc +3 -8
- data/lib/icu_tournament.rb +4 -1
- data/lib/icu_tournament/federation.rb +66 -72
- data/lib/icu_tournament/player.rb +97 -103
- data/lib/icu_tournament/result.rb +82 -89
- data/lib/icu_tournament/team.rb +45 -52
- data/lib/icu_tournament/tournament.rb +153 -163
- data/lib/icu_tournament/tournament_fcsv.rb +123 -129
- data/lib/icu_tournament/tournament_krause.rb +107 -113
- data/lib/icu_tournament/tournament_sp.rb +93 -99
- data/lib/icu_tournament/util.rb +22 -41
- data/lib/icu_tournament/version.rb +3 -1
- metadata +23 -26
- data/lib/icu_tournament/name.rb +0 -274
- data/spec/name_spec.rb +0 -208
|
@@ -1,91 +1,84 @@
|
|
|
1
1
|
# encoding: utf-8
|
|
2
2
|
|
|
3
3
|
module ICU
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
result.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
result.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
result =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
result.rateable
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
tluser
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
tluser.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
result.score = '
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
result.score = '½'
|
|
76
|
-
|
|
77
|
-
The _points_ read-only accessor always returns a floating point number: either 0.0, 0.5 or 1.0.
|
|
78
|
-
|
|
79
|
-
=end
|
|
80
|
-
|
|
4
|
+
#
|
|
5
|
+
# A result is the outcome of a game from the perspective of one of the players.
|
|
6
|
+
# If the game was not a bye or a walkover and involved a second player, then
|
|
7
|
+
# that second player will also have a result for the same game, and the two
|
|
8
|
+
# results will be mirror images of each other.
|
|
9
|
+
#
|
|
10
|
+
# A result always involves a round number, a player number and a score, so these
|
|
11
|
+
# three attributes must be supplied in the constructor.
|
|
12
|
+
#
|
|
13
|
+
# result = ICU::Result.new(2, 10, 'W')
|
|
14
|
+
#
|
|
15
|
+
# The above example represents player 10 winning in round 2. As it stands, it represends
|
|
16
|
+
# a bye or walkover since there is no opponent. Without an opponent, it is unrateable.
|
|
17
|
+
#
|
|
18
|
+
# result.rateable # => false
|
|
19
|
+
#
|
|
20
|
+
# The player's colour and the number of their opponent can be set as follows:
|
|
21
|
+
#
|
|
22
|
+
# result.colour = 'B'
|
|
23
|
+
# result.opponent = 13
|
|
24
|
+
#
|
|
25
|
+
# Specifying an opponent always makes a result rateable.
|
|
26
|
+
#
|
|
27
|
+
# result.rateable # => true
|
|
28
|
+
#
|
|
29
|
+
# This example now represents a win by player 10 with the black pieces over player number 13 in round 2.
|
|
30
|
+
# Alternatively, all this can been specified in the constructor.
|
|
31
|
+
#
|
|
32
|
+
# result = ICU::Result.new(2, 10, 'W', :opponent => 13, :colour => 'B')
|
|
33
|
+
#
|
|
34
|
+
# To make a game unratable, even if it involves an opponent, set the _rateable_ atribute explicity:
|
|
35
|
+
#
|
|
36
|
+
# result.rateable = false
|
|
37
|
+
#
|
|
38
|
+
# or include it in the constructor:
|
|
39
|
+
#
|
|
40
|
+
# result = ICU::Result.new(2, 10, 'W', :opponent => 13, :colour => 'B', :rateable => false)
|
|
41
|
+
#
|
|
42
|
+
# The result of the same game from the perspective of the opponent is:
|
|
43
|
+
#
|
|
44
|
+
# tluser = result.reverse
|
|
45
|
+
#
|
|
46
|
+
# which, with the above example, would be:
|
|
47
|
+
#
|
|
48
|
+
# tluser.player # => 13
|
|
49
|
+
# tluser.opponent # => 10
|
|
50
|
+
# tluser.score # => 'L'
|
|
51
|
+
# tluser.colour # => 'B'
|
|
52
|
+
# tluser.round # => 2
|
|
53
|
+
#
|
|
54
|
+
# The reversed result copies the _rateable_ attribute of the original unless an
|
|
55
|
+
# explicit override is supplied.
|
|
56
|
+
#
|
|
57
|
+
# result.rateable # => true
|
|
58
|
+
# result.reverse.rateable # => true (copied from original)
|
|
59
|
+
# result.reverse(false).rateable # => false (overriden)
|
|
60
|
+
#
|
|
61
|
+
# A result which has no opponent is not reversible (the _reverse_ method returns _nil_).
|
|
62
|
+
#
|
|
63
|
+
# The return value from the _score_ method is always one of _W_, _L_ or _D_. However,
|
|
64
|
+
# when setting the score, a certain amount of variation is permitted as long as it is
|
|
65
|
+
# clear what is meant. For eample, the following would all be converted to _D_:
|
|
66
|
+
#
|
|
67
|
+
# result.score = ' D '
|
|
68
|
+
# result.score = 'd'
|
|
69
|
+
# result.score = '='
|
|
70
|
+
# result.score = '0.5'
|
|
71
|
+
# result.score = '½'
|
|
72
|
+
#
|
|
73
|
+
# The _points_ read-only accessor always returns a floating point number: either 0.0, 0.5 or 1.0.
|
|
74
|
+
#
|
|
81
75
|
class Result
|
|
82
|
-
|
|
83
76
|
extend ICU::Accessor
|
|
84
77
|
attr_positive :round
|
|
85
78
|
attr_integer :player
|
|
86
|
-
|
|
79
|
+
|
|
87
80
|
attr_reader :score, :colour, :opponent, :rateable
|
|
88
|
-
|
|
81
|
+
|
|
89
82
|
# Constructor. Round number, player number and score must be supplied.
|
|
90
83
|
# Optional hash attribute are _opponent_, _colour_ and _rateable_.
|
|
91
84
|
def initialize(round, player, score, opt={})
|
|
@@ -95,7 +88,7 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
|
95
88
|
[:colour, :opponent].each { |a| self.send("#{a}=", opt[a]) unless opt[a].nil? }
|
|
96
89
|
self.rateable = opt[:rateable] # always attempt to set this, and do it last, to get the right default
|
|
97
90
|
end
|
|
98
|
-
|
|
91
|
+
|
|
99
92
|
# Score for the game, even if a default. One of 'W', 'L' or 'D'. Reasonable inputs like 1, 0, =, ½, etc will be converted.
|
|
100
93
|
def score=(score)
|
|
101
94
|
@score = case score.to_s.strip
|
|
@@ -105,7 +98,7 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
|
105
98
|
else raise "invalid score (#{score})"
|
|
106
99
|
end
|
|
107
100
|
end
|
|
108
|
-
|
|
101
|
+
|
|
109
102
|
# Return the score as a floating point number.
|
|
110
103
|
def points
|
|
111
104
|
case @score
|
|
@@ -114,7 +107,7 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
|
114
107
|
else 0.5
|
|
115
108
|
end
|
|
116
109
|
end
|
|
117
|
-
|
|
110
|
+
|
|
118
111
|
# Colour. Either 'W' (white) or 'B' (black).
|
|
119
112
|
def colour=(colour)
|
|
120
113
|
@colour = case colour.to_s
|
|
@@ -124,7 +117,7 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
|
124
117
|
else raise "invalid colour (#{colour})"
|
|
125
118
|
end
|
|
126
119
|
end
|
|
127
|
-
|
|
120
|
+
|
|
128
121
|
# Opponent player number. Either absent (_nil_) or any integer except the player number.
|
|
129
122
|
def opponent=(opponent)
|
|
130
123
|
@opponent = case opponent
|
|
@@ -137,7 +130,7 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
|
137
130
|
raise "opponent number and player number (#{@opponent}) must be different" if @opponent == player
|
|
138
131
|
self.rateable = true if @opponent
|
|
139
132
|
end
|
|
140
|
-
|
|
133
|
+
|
|
141
134
|
# Rateable flag. If false, result is not rateable. Can only be true if there is an opponent.
|
|
142
135
|
def rateable=(rateable)
|
|
143
136
|
if opponent.nil?
|
|
@@ -150,7 +143,7 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
|
150
143
|
else true
|
|
151
144
|
end
|
|
152
145
|
end
|
|
153
|
-
|
|
146
|
+
|
|
154
147
|
# Return a reversed version (from the opponent's perspective) of a result.
|
|
155
148
|
def reverse(rateable=nil)
|
|
156
149
|
return unless @opponent
|
|
@@ -161,7 +154,7 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
|
161
154
|
r.rateable = rateable || @rateable
|
|
162
155
|
r
|
|
163
156
|
end
|
|
164
|
-
|
|
157
|
+
|
|
165
158
|
# Renumber the player and opponent (if there is one) according to the supplied hash. Return self.
|
|
166
159
|
def renumber(map)
|
|
167
160
|
raise "result player number #{@player} not found in renumbering hash" unless map[@player]
|
|
@@ -174,7 +167,7 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
|
174
167
|
end
|
|
175
168
|
self
|
|
176
169
|
end
|
|
177
|
-
|
|
170
|
+
|
|
178
171
|
# Loose equality. True if the round, player and opponent numbers, colour and score all match.
|
|
179
172
|
def ==(other)
|
|
180
173
|
return unless other.is_a? Result
|
|
@@ -183,7 +176,7 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
|
183
176
|
end
|
|
184
177
|
true
|
|
185
178
|
end
|
|
186
|
-
|
|
179
|
+
|
|
187
180
|
# Strict equality. True if the there's loose equality and also the rateablity is the same.
|
|
188
181
|
def eql?(other)
|
|
189
182
|
return true if equal?(other)
|
data/lib/icu_tournament/team.rb
CHANGED
|
@@ -1,65 +1,58 @@
|
|
|
1
1
|
module ICU
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
team
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
team
|
|
18
|
-
|
|
19
|
-
team.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
team.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
team.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
team.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
team.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
team.add(nil) # exception - not a number
|
|
43
|
-
team.add(3) # exception - already a member
|
|
44
|
-
|
|
45
|
-
=end
|
|
46
|
-
|
|
2
|
+
#
|
|
3
|
+
# A team consists of a name and one or more players referenced by numbers.
|
|
4
|
+
# Typically the team will be attached to a tournament (ICU::Tournament)
|
|
5
|
+
# and the numbers will the unique numbers by which the players in that
|
|
6
|
+
# tournament are referenced. To instantiate a team, you must supply a
|
|
7
|
+
# name.
|
|
8
|
+
#
|
|
9
|
+
# team = ICU::Team.new('Wandering Dragons')
|
|
10
|
+
#
|
|
11
|
+
# Then you simply add player's (numbers) to it.
|
|
12
|
+
#
|
|
13
|
+
# team.add_player(1)
|
|
14
|
+
# team.add_payeer(3)
|
|
15
|
+
# team.add_player(7)
|
|
16
|
+
#
|
|
17
|
+
# To get the current members of a team
|
|
18
|
+
#
|
|
19
|
+
# team.members # => [1, 3, 7]
|
|
20
|
+
#
|
|
21
|
+
# You can enquire whether a team contains a given player number.
|
|
22
|
+
#
|
|
23
|
+
# team.contains?(3) # => true
|
|
24
|
+
# team.contains?(4) # => false
|
|
25
|
+
#
|
|
26
|
+
# Or whether it matches a given name (which ignoring case and removing spurious whitespace)
|
|
27
|
+
#
|
|
28
|
+
# team.matches(' wandering dragons ') # => true
|
|
29
|
+
# team.matches('Blundering Bishops') # => false
|
|
30
|
+
#
|
|
31
|
+
# Whenever you reset the name of a tournament spurious whitespace is removed but case is not altered.
|
|
32
|
+
#
|
|
33
|
+
# team.name = ' blundering bishops '
|
|
34
|
+
# team.name # => "blundering bishops"
|
|
35
|
+
#
|
|
36
|
+
# Attempting to add non-numbers or duplicate numbers as new team members results in an exception.
|
|
37
|
+
#
|
|
38
|
+
# team.add(nil) # exception - not a number
|
|
39
|
+
# team.add(3) # exception - already a member
|
|
40
|
+
#
|
|
47
41
|
class Team
|
|
48
|
-
|
|
49
42
|
attr_reader :name, :members
|
|
50
|
-
|
|
43
|
+
|
|
51
44
|
# Constructor. Name must be supplied.
|
|
52
45
|
def initialize(name)
|
|
53
46
|
self.name = name
|
|
54
47
|
@members = Array.new
|
|
55
48
|
end
|
|
56
|
-
|
|
49
|
+
|
|
57
50
|
# Set name. Must not be blank.
|
|
58
51
|
def name=(name)
|
|
59
52
|
@name = name.strip.squeeze(' ')
|
|
60
53
|
raise "team can't be blank" if @name.length == 0
|
|
61
54
|
end
|
|
62
|
-
|
|
55
|
+
|
|
63
56
|
# Add a team member referenced by any integer.
|
|
64
57
|
def add_member(number)
|
|
65
58
|
pnum = number.to_i
|
|
@@ -67,7 +60,7 @@ Attempting to add non-numbers or duplicate numbers as new team members results i
|
|
|
67
60
|
raise "can't add duplicate player number #{pnum} to team '#{@name}'" if @members.include?(pnum)
|
|
68
61
|
@members.push(pnum)
|
|
69
62
|
end
|
|
70
|
-
|
|
63
|
+
|
|
71
64
|
# Renumber the players according to the supplied hash. Return self.
|
|
72
65
|
def renumber(map)
|
|
73
66
|
@members.each_with_index do |pnum, index|
|
|
@@ -76,12 +69,12 @@ Attempting to add non-numbers or duplicate numbers as new team members results i
|
|
|
76
69
|
end
|
|
77
70
|
self
|
|
78
71
|
end
|
|
79
|
-
|
|
72
|
+
|
|
80
73
|
# Detect if a member exists in a team.
|
|
81
74
|
def include?(number)
|
|
82
75
|
@members.include?(number)
|
|
83
76
|
end
|
|
84
|
-
|
|
77
|
+
|
|
85
78
|
# Does the team name match the given string (ignoring case and spurious whitespace).
|
|
86
79
|
def matches(name)
|
|
87
80
|
self.name.downcase == name.strip.squeeze(' ').downcase
|
|
@@ -1,167 +1,157 @@
|
|
|
1
1
|
module ICU
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
t
|
|
20
|
-
|
|
21
|
-
t.
|
|
22
|
-
t.
|
|
23
|
-
t.
|
|
24
|
-
|
|
25
|
-
t.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
puts
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* the
|
|
68
|
-
* the
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
t.rerank
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
t = ICU::Tournament.parse_file!('champs.zip', 'SwissPerfect', :start => '2010-07-03')
|
|
155
|
-
|
|
156
|
-
The method takes a filename, format and an options hash as arguments. It either returns
|
|
157
|
-
an instance of ICU::Tournament or throws an exception. See the documentation for the
|
|
158
|
-
different formats for what options are available. For some, no options are available,
|
|
159
|
-
in which case any options supplied to this method will be silently ignored.
|
|
160
|
-
|
|
161
|
-
=end
|
|
162
|
-
|
|
2
|
+
#
|
|
3
|
+
# One way to create a tournament object is by parsing one of the supported file types (e.g. ICU::Tournament::Krause).
|
|
4
|
+
# It is also possible to build one programmatically by:
|
|
5
|
+
#
|
|
6
|
+
# * creating a bare tournament instance,
|
|
7
|
+
# * adding all the players,
|
|
8
|
+
# * adding all the results.
|
|
9
|
+
#
|
|
10
|
+
# For example:
|
|
11
|
+
#
|
|
12
|
+
# require 'rubygems'
|
|
13
|
+
# require 'icu_tournament'
|
|
14
|
+
#
|
|
15
|
+
# t = ICU::Tournament.new('Bangor Masters', '2009-11-09')
|
|
16
|
+
#
|
|
17
|
+
# t.add_player(ICU::Player.new('Bobby', 'Fischer', 10))
|
|
18
|
+
# t.add_player(ICU::Player.new('Garry', 'Kasparov', 20))
|
|
19
|
+
# t.add_player(ICU::Player.new('Mark', 'Orr', 30))
|
|
20
|
+
#
|
|
21
|
+
# t.add_result(ICU::Result.new(1, 10, 'D', :opponent => 30, :colour => 'W'))
|
|
22
|
+
# t.add_result(ICU::Result.new(2, 20, 'W', :opponent => 30, :colour => 'B'))
|
|
23
|
+
# t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :colour => 'W'))
|
|
24
|
+
#
|
|
25
|
+
# t.validate!(:rerank => true)
|
|
26
|
+
#
|
|
27
|
+
# and then:
|
|
28
|
+
#
|
|
29
|
+
# serializer = ICU::Tournament::Krause.new
|
|
30
|
+
# puts serializer.serialize(@t)
|
|
31
|
+
#
|
|
32
|
+
# or equivalntly, just:
|
|
33
|
+
#
|
|
34
|
+
# puts t.serialize('Krause')
|
|
35
|
+
#
|
|
36
|
+
# would result in the following output:
|
|
37
|
+
#
|
|
38
|
+
# 012 Bangor Masters
|
|
39
|
+
# 042 2009-11-09
|
|
40
|
+
# 001 10 Fischer,Bobby 1.5 1 30 w = 20 b 1
|
|
41
|
+
# 001 20 Kasparov,Garry 1.0 2 30 b 1 10 w 0
|
|
42
|
+
# 001 30 Orr,Mark 0.5 3 10 b = 20 w 0
|
|
43
|
+
#
|
|
44
|
+
# Note that the players should be added first because the _add_result_ method will
|
|
45
|
+
# raise an exception if the players it references through their tournament numbers
|
|
46
|
+
# (10, 20 and 30 in this example) have not already been added to the tournament.
|
|
47
|
+
#
|
|
48
|
+
# See ICU::Player and ICU::Result for more details about players and results.
|
|
49
|
+
#
|
|
50
|
+
# == Validation
|
|
51
|
+
#
|
|
52
|
+
# A tournament can be validated with either the <em>validate!</em> or _invalid_ methods.
|
|
53
|
+
# On success, the first returns true while the second returns false.
|
|
54
|
+
# On error, the first throws an exception while the second returns a string
|
|
55
|
+
# describing the error.
|
|
56
|
+
#
|
|
57
|
+
# Validations checks that:
|
|
58
|
+
#
|
|
59
|
+
# * there are at least two players
|
|
60
|
+
# * every player has a least one result
|
|
61
|
+
# * the result round numbers are consistent (no more than one game per player per round)
|
|
62
|
+
# * the tournament dates (start, finish, round dates), if there are any, are consistent
|
|
63
|
+
# * the player ranks are consistent with their scores
|
|
64
|
+
#
|
|
65
|
+
# Side effects of calling <em>validate!</em> or _invalid_ include:
|
|
66
|
+
#
|
|
67
|
+
# * the number of rounds will be set if not set already
|
|
68
|
+
# * the finish date will be set if not set already and if there are round dates
|
|
69
|
+
#
|
|
70
|
+
# Optionally, additional validation checks can be performed given a tournament
|
|
71
|
+
# parser/serializer. For example:
|
|
72
|
+
#
|
|
73
|
+
# t.validate!(:type => ICU::Tournament.ForeignCSV.new)
|
|
74
|
+
#
|
|
75
|
+
# Or equivalently:
|
|
76
|
+
#
|
|
77
|
+
# t.validate!(:type => 'ForeignCSV')
|
|
78
|
+
#
|
|
79
|
+
# Such additional validation is always performed before a tournament is serialized.
|
|
80
|
+
# For example, the following are equivalent and will throw an exception if
|
|
81
|
+
# the tournament is invalid according to either the general rules or the rules
|
|
82
|
+
# specific for the type used:
|
|
83
|
+
#
|
|
84
|
+
# t.serialize('ForeignCSV')
|
|
85
|
+
# ICU::Tournament::ForeignCSV.new.serialize(t)
|
|
86
|
+
#
|
|
87
|
+
# == Ranking
|
|
88
|
+
#
|
|
89
|
+
# The players in a tournament can be ranked by calling the _rerank_ method directly.
|
|
90
|
+
#
|
|
91
|
+
# t.rerank
|
|
92
|
+
#
|
|
93
|
+
# Alternatively they can be ranked as a side effect of validation if the _rerank_ option is set,
|
|
94
|
+
# but this only applies if the tournament is not yet ranked or it's ranking is inconsistent.
|
|
95
|
+
#
|
|
96
|
+
# t.validate(:rerank => true)
|
|
97
|
+
#
|
|
98
|
+
# Ranking is inconsistent if some but not all players have a rank or if all players
|
|
99
|
+
# have a rank but some are ranked higher than others on lower scores.
|
|
100
|
+
#
|
|
101
|
+
# To rank the players requires a tie break method to be specified to order players on the same score.
|
|
102
|
+
# The default is alphabetical (by last name then first name). Other methods can be specified by supplying
|
|
103
|
+
# an array of methods (strings or symbols) in order of precedence to the _tie_breaks_ setter. Examples:
|
|
104
|
+
#
|
|
105
|
+
# t.tie_breaks = ['Sonneborn-Berger']
|
|
106
|
+
# t.tie_breaks = [:buchholz, :neustadtl, :blacks, :wins]
|
|
107
|
+
# t.tie_breaks = [] # reset to the default
|
|
108
|
+
#
|
|
109
|
+
# The full list of supported methods is:
|
|
110
|
+
#
|
|
111
|
+
# * _Buchholz_: sum of opponents' scores
|
|
112
|
+
# * _Harkness_ (or _median_): like Buchholz except the highest and lowest opponents' scores are discarded (or two highest and lowest if 9 rounds or more)
|
|
113
|
+
# * _modified_median_: same as Harkness except only lowest (or highest) score(s) are discarded for players with more (or less) than 50%
|
|
114
|
+
# * _Neustadtl_ (or _Sonneborn-Berger_): sum of scores of players defeated plus half sum of scores of players drawn against
|
|
115
|
+
# * _progressive_ (or _cumulative_): sum of running score for each round
|
|
116
|
+
# * _ratings_: sum of opponents ratings
|
|
117
|
+
# * _blacks_: number of blacks
|
|
118
|
+
# * _wins_: number of wins
|
|
119
|
+
# * _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)
|
|
120
|
+
#
|
|
121
|
+
# The return value from _rerank_ is the tournament object itself, to allow chaining, for example:
|
|
122
|
+
#
|
|
123
|
+
# t.rerank.renumber
|
|
124
|
+
#
|
|
125
|
+
# == Renumbering
|
|
126
|
+
#
|
|
127
|
+
# The numbers used to uniquely identify each player in a tournament can be any set of unique integers
|
|
128
|
+
# (including zero and negative numbers). To renumber the players so that these numbers start at 1 and
|
|
129
|
+
# end with the total number of players, use the _renumber_ method. This method takes one optional
|
|
130
|
+
# argument to specify how the renumbering is done.
|
|
131
|
+
#
|
|
132
|
+
# t.renumber(:rank) # renumber by rank (if there are consistent rankings), otherwise by name alphabetically
|
|
133
|
+
# t.renumber # the same, as renumbering by rank is the default
|
|
134
|
+
# t.renumber(:name) # renumber by name alphabetically
|
|
135
|
+
# t.renumber(:order) # renumber maintaining the order of the original numbers
|
|
136
|
+
#
|
|
137
|
+
# The return value from _renumber_ is the tournament object itself.
|
|
138
|
+
#
|
|
139
|
+
# == Parsing Files
|
|
140
|
+
#
|
|
141
|
+
# As an alternative to processing files by first instantiating a parser of the appropropriate class
|
|
142
|
+
# (such as ICU::Tournament::SwissPerfect, ICU::Tournament::Krause and ICU::Tournament::ForeignCSV)
|
|
143
|
+
# and then calling the parser's <em>parse_file</em> or <em>parse_file!</em> instance method,
|
|
144
|
+
# a convenience class method, <em>parse_file!</em>, is available when a parser instance is not required.
|
|
145
|
+
# For example:
|
|
146
|
+
#
|
|
147
|
+
# t = ICU::Tournament.parse_file!('champs.zip', 'SwissPerfect', :start => '2010-07-03')
|
|
148
|
+
#
|
|
149
|
+
# The method takes a filename, format and an options hash as arguments. It either returns
|
|
150
|
+
# an instance of ICU::Tournament or throws an exception. See the documentation for the
|
|
151
|
+
# different formats for what options are available. For some, no options are available,
|
|
152
|
+
# in which case any options supplied to this method will be silently ignored.
|
|
153
|
+
#
|
|
163
154
|
class Tournament
|
|
164
|
-
|
|
165
155
|
extend ICU::Accessor
|
|
166
156
|
attr_date :start
|
|
167
157
|
attr_date_or_nil :finish
|
|
@@ -389,7 +379,7 @@ in which case any options supplied to this method will be silently ignored.
|
|
|
389
379
|
check_type(options[:type]) if options[:type]
|
|
390
380
|
true
|
|
391
381
|
end
|
|
392
|
-
|
|
382
|
+
|
|
393
383
|
# Convenience method to parse a file.
|
|
394
384
|
def self.parse_file!(file, format, opts={})
|
|
395
385
|
type = format.to_s
|