Class: ICU::Tournament

Inherits:
Object
  • Object
show all
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.7"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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

#fedObject

Returns the value of attribute fed.



204
205
206
# File 'lib/icu_tournament/tournament.rb', line 204

def fed
  @fed
end

#round_datesObject (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

#siteObject

Returns the value of attribute site.



204
205
206
# File 'lib/icu_tournament/tournament.rb', line 204

def site
  @site
end

#teamsObject (readonly)

Returns the value of attribute teams.



204
205
206
# File 'lib/icu_tournament/tournament.rb', line 204

def teams
  @teams
end

#tie_breaksObject

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(options={})
  begin
    validate!(options)
  rescue => err
    return err.message
  end
  false
end

#last_roundObject

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

#playersObject

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

#rerankObject

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_scoresObject

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!(options={})
  begin check_ranks rescue rerank end if options[:rerank]
  check_players
  check_rounds
  check_dates
  check_teams
  check_ranks(:allow_none => true)
  check_type(options[:type]) if options[:type]
  true
end