Class: ICU::Tournament
- Inherits:
-
Object
- Object
- ICU::Tournament
- Extended by:
- Util::Accessor
- Defined in:
- lib/icu_tournament/tournament.rb,
lib/icu_tournament/version.rb,
lib/icu_tournament/tournament_sp.rb,
lib/icu_tournament/tournament_spx.rb,
lib/icu_tournament/tournament_fcsv.rb,
lib/icu_tournament/tournament_krause.rb
Overview
One way to create a tournament object is by parsing one of the supported file types (e.g. ICU::Tournament::Krause). It is also possible to build one programmatically by:
-
creating a bare tournament instance,
-
adding all the players,
-
adding all the results.
For example:
require 'icu_tournament'
t = ICU::Tournament.new('Bangor Masters', '2009-11-09')
t.add_player(ICU::Player.new('Bobby', 'Fischer', 10))
t.add_player(ICU::Player.new('Garry', 'Kasparov', 20))
t.add_player(ICU::Player.new('Mark', 'Orr', 30))
t.add_result(ICU::Result.new(1, 10, 'D', :opponent => 30, :colour => 'W'))
t.add_result(ICU::Result.new(2, 20, 'W', :opponent => 30, :colour => 'B'))
t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :colour => 'W'))
t.validate!(:rerank => true)
and then:
serializer = ICU::Tournament::Krause.new
puts serializer.serialize(@t)
or equivalntly, just:
puts t.serialize('Krause')
would result in the following output:
012 Bangor Masters
042 2009-11-09
001 10 Fischer,Bobby 1.5 1 30 w = 20 b 1
001 20 Kasparov,Garry 1.0 2 30 b 1 10 w 0
001 30 Orr,Mark 0.5 3 10 b = 20 w 0
Note that the players should be added first because the add_result method will raise an exception if the players it references through their tournament numbers (10, 20 and 30 in this example) have not already been added to the tournament.
Adding a result from the perspective of one player automatically adds it from the perspective of the opponent, if there is one. The result may subsequently be added explicitly from opponent’s perspective as long as it does not contradict what was implicitly added previously.
t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :colour => 'W'))
t.add_result(ICU::Result.new(3, 10, 'W', :opponent => 20, :colour => 'B')) # unnecessary, but not a problem
t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :colour => 'W'))
t.add_result(ICU::Result.new(3, 10, 'D', :opponent => 20, :colour => 'B')) # would raise an exception
Asymmetric Scores
There is one exception to the rule that two corresponding results must be consistent: if both results are unrateable then the two scores need not sum to 1. The commonest case this caters for is probably that of a double default. To create such asymmetric results you must add the result from both players’ perspectives. For example:
t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :rateable => false))
t.add_result(ICU::Result.new(3, 10, 'L', :opponent => 20, :rateable => false))
After the first add_result the two results are, as usual, consistent (in particular, the loss for player 20 is balanced by a win for player 10). However, the second add_result, which asserts player 10 lost, does not cause an exception. It would have done if the results had been rateable but, because they are not, the scores are allowed to add up to something other than 1.0 (in this case, zero) and the effect of the second call to add_result is merely to adjust the score of player 10 from a win to a loss (while maintaining the loss for player 20).
See ICU::Player and ICU::Result for more details about players and results.
Tournament Dates
A tournament start date is mandatory and supplied in the constructor. Finish and round dates are optional. To supply a finish date, supply it in constructor arguments or set it explicityly.
t = ICU::Tournament.new('Bangor Masters', '2009-11-09', :finish => '2009-11-11')
t.finish = '2009-11-11'
To set round dates, add the correct number in the correct order one at a time.
t.add_round_date('2009-11-09')
t.add_round_date('2009-11-10')
t.add_round_date('2009-11-11')
Validation
A tournament can be validated with either the validate! or invalid methods. On success, the first returns true while the second returns false. On error, the first throws an exception while the second returns a string describing the error.
Validations checks that:
-
there are at least two players
-
result round numbers are consistent (no more than one game per player per round)
-
corresponding results are consistent (although they may have asymmetric scores if unrateable, as previously desribed)
-
the tournament dates (start, finish, round dates), if there are any, are consistent
-
player ranks are consistent with their scores
-
there are no players with duplicate ICU IDs or duplicate FIDE IDs
Side effects of calling validate! or invalid include:
-
the number of rounds will be set if not set already
-
the finish date will be set if not set already and if there are round dates
Optionally, additional validation checks, appropriate for a given serializer, may be performed. For example:
t.validate!(:type => ICU::Tournament.ForeignCSV.new)
or equivalently,
t.validate!(:type => 'ForeignCSV')
which, amongst other tests, checks that there is at least one player with an ICU number and that all such players have a least one game against a FIDE rated opponent. This is an example of a specialized check that is only appropriate for a particular serializer. If it raises an exception then the tournament cannot be serialized that way.
Validation is automatically performed just before a tournament is serialized. For example, the following are equivalent and will throw an exception if the tournament is invalid according to either the general rules or the rules specific for the type used:
t.serialize('ForeignCSV')
ICU::Tournament::ForeignCSV.new.serialize(t)
Ranking
The players in a tournament can be ranked by calling the rerank method directly.
t.rerank
Alternatively they can be ranked as a side effect of validation if the rerank option is set, but this only applies if the tournament is not yet ranked or it’s ranking is inconsistent.
t.validate(:rerank => true)
Ranking is inconsistent if not all players have a rank or at least one pair of players exist where one has a higher score but a lower rank.
To rank the players requires one or more tie break methods for ordering players on the same score. Methods can be specified by supplying an array of methods names (strings or symbols) in order of precedence to the tie_breaks setter. Examples:
t.tie_breaks = ['Sonneborn-Berger']
t.tie_breaks = [:buchholz, :neustadtl, :blacks, :wins]
t.tie_breaks = [] # use the default - see below
If the first method fails to differentiate two tied players, the second is tried, and then the third and so on. See ICU::TieBreak for the full list of supported tie break methods.
Unless explicity specified, the name tie break (which orders alphabetically by last name then first name) is implicitly used as a method of last resort. Thus, in the absence of any tie break methods being specified at all, alphabetical ordering is the default.
The return value from rerank is the tournament object itself, to allow method chaining, for example:
t.rerank.renumber
Renumbering
The numbers used to uniquely identify each player in a tournament can be any set of unique integers (including zero and negative numbers). To renumber the players so that these numbers start at 1 and end with the total number of players, use the renumber method. This method takes one optional argument to specify how the renumbering is done.
t.renumber(:rank) # renumber by rank (if there are consistent rankings), otherwise by name alphabetically
t.renumber # the same, as renumbering by rank is the default
t.renumber(:name) # renumber by name alphabetically
t.renumber(:order) # renumber maintaining the order of the original numbers
The return value from renumber is the tournament object itself.
Parsing Files
As an alternative to processing files by first instantiating a parser of the appropropriate class (such as ICU::Tournament::SwissPerfect, ICU::Tournament::Krause and ICU::Tournament::ForeignCSV) and then calling the parser’s parse_file or parse_file! instance method, a convenience class method, parse_file!, is available when a parser instance is not required. For example:
t = ICU::Tournament.parse_file!('champs.zip', 'SwissPerfect', :start => '2010-07-03')
The method takes a filename, format and an options hash as arguments. It either returns an instance of ICU::Tournament or throws an exception. See the documentation for the different formats for what options are available. For some, no options are available, in which case any options supplied to this method will be silently ignored.
Defined Under Namespace
Classes: ForeignCSV, Krause, SPExport, SwissPerfect
Constant Summary collapse
- VERSION =
"1.9.3"
Instance Attribute Summary collapse
-
#fed ⇒ Object
Returns the value of attribute fed.
-
#round_dates ⇒ Object
readonly
Returns the value of attribute round_dates.
-
#site ⇒ Object
Returns the value of attribute site.
-
#teams ⇒ Object
readonly
Returns the value of attribute teams.
-
#tie_breaks ⇒ Object
Returns the value of attribute tie_breaks.
Class Method Summary collapse
-
.parse_file!(file, format, opts = {}) ⇒ Object
Convenience method to parse a file.
Instance Method Summary collapse
-
#add_player(player) ⇒ Object
Add a new player to the tournament.
-
#add_result(result) ⇒ Object
Add a result to a tournament.
-
#add_round_date(round_date) ⇒ Object
Add a round date.
-
#add_team(team) ⇒ Object
Add a new team.
-
#find_player(player) ⇒ Object
Lookup a player in the tournament by player number, returning nil if the player number does not exist.
-
#get_team(name) ⇒ Object
Return the team object that matches a given name, or nil if not found.
-
#initialize(name, start, opt = {}) ⇒ Tournament
constructor
Constructor.
-
#invalid(options = {}) ⇒ Object
Is a tournament invalid? Either returns false (if it’s valid) or an error message.
-
#last_round ⇒ Object
Return the greatest round number according to the players results (which may not be the same as the set number of rounds).
-
#player(num) ⇒ Object
Get a player by their number.
-
#players ⇒ Object
Return an array of all players in order of their player number.
-
#renumber(criterion = :rank) ⇒ Object
Renumber the players according to a given criterion.
-
#rerank ⇒ Object
Rerank the tournament by score first and if necessary using a configurable tie breaker method.
-
#round_date(round) ⇒ Object
Return the date of a given round, or nil if unavailable.
-
#serialize(format, arg = {}) ⇒ Object
Convenience method to serialise the tournament into a supported format.
-
#tie_break_scores ⇒ Object
Return a hash (player number to value) of tie break scores for the main method.
-
#validate!(options = {}) ⇒ Object
Raise an exception if a tournament is not valid.
Methods included from Util::Accessor
attr_accessor, attr_date, attr_date_or_nil, attr_integer, attr_integer_or_nil, attr_positive, attr_positive_or_nil, attr_string, attr_string_or_nil
Constructor Details
#initialize(name, start, opt = {}) ⇒ Tournament
Constructor. Name and start date must be supplied. Other attributes are optional.
207 208 209 210 211 212 213 214 215 |
# File 'lib/icu_tournament/tournament.rb', line 207 def initialize(name, start, opt={}) self.name = name self.start = start [:finish, :rounds, :site, :city, :fed, :type, :arbiter, :deputy, :time_control].each { |a| self.send("#{a}=", opt[a]) unless opt[a].nil? } @player = {} @teams = [] @round_dates = [] @tie_breaks = [] end |
Instance Attribute Details
#fed ⇒ Object
Returns the value of attribute fed.
204 205 206 |
# File 'lib/icu_tournament/tournament.rb', line 204 def fed @fed end |
#round_dates ⇒ Object (readonly)
Returns the value of attribute round_dates.
204 205 206 |
# File 'lib/icu_tournament/tournament.rb', line 204 def round_dates @round_dates end |
#site ⇒ Object
Returns the value of attribute site.
204 205 206 |
# File 'lib/icu_tournament/tournament.rb', line 204 def site @site end |
#teams ⇒ Object (readonly)
Returns the value of attribute teams.
204 205 206 |
# File 'lib/icu_tournament/tournament.rb', line 204 def teams @teams end |
#tie_breaks ⇒ Object
Returns the value of attribute tie_breaks.
204 205 206 |
# File 'lib/icu_tournament/tournament.rb', line 204 def tie_breaks @tie_breaks end |
Class Method Details
.parse_file!(file, format, opts = {}) ⇒ Object
Convenience method to parse a file.
407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'lib/icu_tournament/tournament.rb', line 407 def self.parse_file!(file, format, opts={}) type = format.to_s raise "Invalid format" unless klass = factory(format) #type.match(/^(SwissPerfect|SPExport|Krause|ForeignCSV)$/); parser = klass.new if type == 'ForeignCSV' # Doesn't take options. parser.parse_file!(file) else # The others can take options. parser.parse_file!(file, opts) end end |
Instance Method Details
#add_player(player) ⇒ Object
Add a new player to the tournament. Must have a unique player number.
281 282 283 284 285 |
# File 'lib/icu_tournament/tournament.rb', line 281 def add_player(player) raise "invalid player" unless player.class == ICU::Player raise "player number (#{player.num}) should be unique" if @player[player.num] @player[player.num] = player end |
#add_result(result) ⇒ Object
Add a result to a tournament. An exception is raised if the players referenced in the result (by number) do not exist in the tournament. The result, which remember is from the perspective of one of the players, is added to that player’s results. Additionally, the reverse of the result is automatically added to the player’s opponent, if there is one.
306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/icu_tournament/tournament.rb', line 306 def add_result(result) raise "invalid result" unless result.class == ICU::Result raise "result round number (#{result.round}) inconsistent with number of tournament rounds" if @rounds && result.round > @rounds raise "player number (#{result.player}) does not exist" unless @player[result.player] return if add_asymmetric_result?(result) @player[result.player].add_result(result) if result.opponent raise "opponent number (#{result.opponent}) does not exist" unless @player[result.opponent] @player[result.opponent].add_result(result.reverse) end end |
#add_round_date(round_date) ⇒ Object
Add a round date.
225 226 227 228 229 230 |
# File 'lib/icu_tournament/tournament.rb', line 225 def add_round_date(round_date) round_date = round_date.to_s.strip parsed_date = Util::Date.parse(round_date) raise "invalid round date (#{round_date})" unless parsed_date @round_dates << parsed_date end |
#add_team(team) ⇒ Object
Add a new team. The argument is either a team (possibly already with members) or the name of a new team. The team’s name must be unique in the tournament. Returns the the team instance.
258 259 260 261 262 263 |
# File 'lib/icu_tournament/tournament.rb', line 258 def add_team(team) team = Team.new(team.to_s) unless team.is_a? Team raise "a team with a name similar to '#{team.name}' already exists" if self.get_team(team.name) @teams << team team end |
#find_player(player) ⇒ Object
Lookup a player in the tournament by player number, returning nil if the player number does not exist.
298 299 300 |
# File 'lib/icu_tournament/tournament.rb', line 298 def find_player(player) players.find { |p| p == player } end |
#get_team(name) ⇒ Object
Return the team object that matches a given name, or nil if not found.
266 267 268 |
# File 'lib/icu_tournament/tournament.rb', line 266 def get_team(name) @teams.find{ |t| t.matches(name) } end |
#invalid(options = {}) ⇒ Object
Is a tournament invalid? Either returns false (if it’s valid) or an error message. Has the same rerank option as validate!.
384 385 386 387 388 389 390 391 |
# File 'lib/icu_tournament/tournament.rb', line 384 def invalid(={}) begin validate!() rescue => err return err. end false end |
#last_round ⇒ Object
Return the greatest round number according to the players results (which may not be the same as the set number of rounds).
238 239 240 241 242 243 244 245 246 |
# File 'lib/icu_tournament/tournament.rb', line 238 def last_round last_round = 0 @player.values.each do |p| p.results.each do |r| last_round = r.round if r.round > last_round end end last_round end |
#player(num) ⇒ Object
Get a player by their number.
288 289 290 |
# File 'lib/icu_tournament/tournament.rb', line 288 def player(num) @player[num] end |
#players ⇒ Object
Return an array of all players in order of their player number.
293 294 295 |
# File 'lib/icu_tournament/tournament.rb', line 293 def players @player.values.sort_by{ |p| p.num } end |
#renumber(criterion = :rank) ⇒ Object
Renumber the players according to a given criterion.
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/icu_tournament/tournament.rb', line 343 def renumber(criterion = :rank) if (criterion.class == Hash) # Undocumentted feature - supply your own hash. map = criterion else # Official way of reordering. map = Hash.new # Renumber by rank only if possible. criterion = criterion.to_s.downcase if criterion == 'rank' begin check_ranks rescue criterion = 'name' end end # Decide how to renumber. if criterion == 'rank' # Renumber by rank. @player.values.each{ |p| map[p.num] = p.rank } elsif criterion == 'order' # Just keep the existing numbers in order. @player.values.sort_by{ |p| p.num }.each_with_index{ |p, i| map[p.num] = i + 1 } else # Renumber by name alphabetically. @player.values.sort_by{ |p| p.name }.each_with_index{ |p, i| map[p.num] = i + 1 } end end # Apply renumbering. @teams.each{ |t| t.renumber(map) } @player = @player.values.inject({}) do |hash, player| player.renumber(map) hash[player.num] = player hash end # Return self for chaining. self end |
#rerank ⇒ Object
Rerank the tournament by score first and if necessary using a configurable tie breaker method.
319 320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'lib/icu_tournament/tournament.rb', line 319 def rerank tie_break_methods, tie_break_order, tie_break_hash = tie_break_data @player.values.sort do |a,b| cmp = 0 tie_break_methods.each do |m| cmp = (tie_break_hash[m][a.num] <=> tie_break_hash[m][b.num]) * tie_break_order[m] if cmp == 0 end cmp end.each_with_index do |p,i| p.rank = i + 1 end self end |
#round_date(round) ⇒ Object
Return the date of a given round, or nil if unavailable.
233 234 235 |
# File 'lib/icu_tournament/tournament.rb', line 233 def round_date(round) @round_dates[round-1] end |
#serialize(format, arg = {}) ⇒ Object
Convenience method to serialise the tournament into a supported format. Throws an exception unless the name of a supported format is supplied or if the tournament is unsuitable for serialisation in that format.
423 424 425 426 427 428 429 430 431 432 |
# File 'lib/icu_tournament/tournament.rb', line 423 def serialize(format, arg={}) serializer = case format.to_s.downcase when 'krause' then ICU::Tournament::Krause.new when 'foreigncsv' then ICU::Tournament::ForeignCSV.new when 'spexport' then ICU::Tournament::SPExport.new when '' then raise "no format supplied" else raise "unsupported serialisation format: '#{format}'" end serializer.serialize(self, arg) end |
#tie_break_scores ⇒ Object
Return a hash (player number to value) of tie break scores for the main method.
334 335 336 337 338 339 340 |
# File 'lib/icu_tournament/tournament.rb', line 334 def tie_break_scores tie_break_methods, tie_break_order, tie_break_hash = tie_break_data main_method = tie_break_methods[1] scores = Hash.new @player.values.each { |p| scores[p.num] = tie_break_hash[main_method][p.num] } scores end |
#validate!(options = {}) ⇒ Object
Raise an exception if a tournament is not valid. The rerank option can be set to true to rank the tournament just prior to the test if ranking data is missing or inconsistent.
395 396 397 398 399 400 401 402 403 404 |
# File 'lib/icu_tournament/tournament.rb', line 395 def validate!(={}) begin check_ranks rescue rerank end if [:rerank] check_players check_rounds check_dates check_teams check_ranks(:allow_none => true) check_type([:type]) if [:type] true end |