icu_tournament 1.4.3 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -181,11 +181,19 @@ module ICU
181
181
  end
182
182
  end
183
183
 
184
- # Lookup a result by round number.
184
+ # Lookup a result by round number and return it (or nil if there is no such result).
185
185
  def find_result(round)
186
186
  @results.find { |r| r.round == round }
187
187
  end
188
188
 
189
+ # Lookup a result by round number, remove it and return it (or return nil if there is no such result).
190
+ def remove_result(round)
191
+ result = find_result(round)
192
+ return unless result
193
+ @results.delete(result)
194
+ result
195
+ end
196
+
189
197
  # Return the player's total points.
190
198
  def points
191
199
  @results.inject(0.0) { |t, r| t += r.points }
@@ -66,6 +66,19 @@ module ICU
66
66
  #
67
67
  # The _points_ read-only accessor always returns a floating point number: either 0.0, 0.5 or 1.0.
68
68
  #
69
+ # Two results are <em>eql?</em> if all there attributes are the same, unless exceptions are specified.
70
+ #
71
+ # r = ICU::Result.new(1, 1, 'W', :opponent => 2)
72
+ # r1 = ICU::Result.new(1, 1, 'W', :opponent => 2)
73
+ # r2 = ICU::Result.new(1, 1, 'W', :opponent => 2, :rateable => false)
74
+ # r3 = ICU::Result.new(1, 1, 'L', :opponent => 2, :rateable => false)
75
+ #
76
+ # r.eql?(r1) # => true
77
+ # r.eql?(r2) # => false
78
+ # r.eql?(r3) # => false
79
+ # r.eql?(r2, :except => :rateable) # => true
80
+ # r.eql?(r3, :except => [:rateable, :score]) # => true
81
+ #
69
82
  class Result
70
83
  extend ICU::Accessor
71
84
  attr_positive :round
@@ -167,20 +180,22 @@ module ICU
167
180
  "R#{@round}P#{@player}O#{@opponent || '-'}#{@score}#{@colour || '-'}#{@rateable ? 'R' : 'U'}"
168
181
  end
169
182
 
170
- # Loose equality. True if the round, player and opponent numbers, colour and score all match.
171
- def ==(other)
183
+ # Equality. True if all attributes equal, exceptions allowed.
184
+ def eql?(other, opt={})
185
+ return true if equal?(other)
172
186
  return unless other.is_a? Result
173
- [:round, :player, :opponent, :colour, :score].each do |m|
174
- return false unless self.send(m) == other.send(m)
187
+ except = Hash.new
188
+ if opt[:except]
189
+ if opt[:except].is_a?(Array)
190
+ opt[:except].each { |x| except[x.to_sym] = true }
191
+ else
192
+ except[opt[:except].to_sym] = true
193
+ end
194
+ end
195
+ [:round, :player, :opponent, :colour, :score, :rateable].each do |m|
196
+ return false unless except[m] || self.send(m) == other.send(m)
175
197
  end
176
198
  true
177
199
  end
178
-
179
- # Strict equality. True if the there's loose equality and also the rateablity is the same.
180
- def eql?(other)
181
- return true if equal?(other)
182
- return false unless self == other
183
- self.rateable == other.rateable
184
- end
185
200
  end
186
201
  end
@@ -55,6 +55,22 @@ module ICU
55
55
  # t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :colour => 'W'))
56
56
  # t.add_result(ICU::Result.new(3, 10, 'D', :opponent => 20, :colour => 'B')) # would raise an exception
57
57
  #
58
+ # == Asymmetric Scores
59
+ #
60
+ # There is one exception to the rule that two corresponding results must be consistent:
61
+ # if both results are unrateable then the two scores need not sum to 1. The commonest case
62
+ # this caters for is probably that of a double default. To create such asymmetric results
63
+ # you must add the result from both players' perspectives. For example:
64
+ #
65
+ # t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :rateable => false))
66
+ # t.add_result(ICU::Result.new(3, 10, 'L', :opponent => 20, :rateable => false))
67
+ #
68
+ # After the first _add_result_ the two results are, as usual, consistent (in particular, the loss for player 20
69
+ # is balanced by a win for player 10). However, the second _add_result_, which asserts player 10 lost, does not cause
70
+ # an exception. It would have done if the results had been rateable but, because they are not, the scores are
71
+ # allowed to add up to something other than 1.0 (in this case, zero) and the effect of the second call to _add_result_
72
+ # is merely to adjust the score of player 10 from a win to a loss (while maintaining the loss for player 20).
73
+ #
58
74
  # See ICU::Player and ICU::Result for more details about players and results.
59
75
  #
60
76
  # == Validation
@@ -67,11 +83,11 @@ module ICU
67
83
  # Validations checks that:
68
84
  #
69
85
  # * there are at least two players
70
- # * every player has a least one result
71
- # * the result round numbers are consistent (no more than one game per player per round)
86
+ # * result round numbers are consistent (no more than one game per player per round)
87
+ # * corresponding results are consistent (although they may have asymmetric scores if unrateable, as previously desribed)
72
88
  # * the tournament dates (start, finish, round dates), if there are any, are consistent
73
- # * the player ranks are consistent with their scores
74
- # * there are no players with duplicate ICU IDs or duplicate FIDE IDs
89
+ # * player ranks are consistent with their scores
90
+ # * there are no players with duplicate \ICU IDs or duplicate \FIDE IDs
75
91
  #
76
92
  # Side effects of calling <em>validate!</em> or _invalid_ include:
77
93
  #
@@ -87,7 +103,7 @@ module ICU
87
103
  #
88
104
  # t.validate!(:type => 'ForeignCSV')
89
105
  #
90
- # which, amongst other tests, checks that there is at least one player with an ICU number and
106
+ # which, amongst other tests, checks that there is at least one player with an \ICU number and
91
107
  # that all such players have a least one game against a FIDE rated opponent. This is an example
92
108
  # of a specialized check that is only appropriate for a particular serializer. If it raises an
93
109
  # exception then the tournament cannot be serialized that way.
@@ -278,6 +294,7 @@ module ICU
278
294
  raise "invalid result" unless result.class == ICU::Result
279
295
  raise "result round number (#{result.round}) inconsistent with number of tournament rounds" if @rounds && result.round > @rounds
280
296
  raise "player number (#{result.player}) does not exist" unless @player[result.player]
297
+ return if add_asymmetric_result?(result)
281
298
  @player[result.player].add_result(result)
282
299
  if result.opponent
283
300
  raise "opponent number (#{result.opponent}) does not exist" unless @player[result.opponent]
@@ -418,14 +435,15 @@ module ICU
418
435
  raise "duplicate FIDE IDs, players #{p.num} and #{fide_ids[p.fide_id]}" if fide_ids[p.fide_id]
419
436
  fide_ids[p.fide_id] = num
420
437
  end
421
- raise "player #{num} has no results" if p.results.size == 0
438
+ return if p.results.size == 0
422
439
  p.results.each do |r|
423
440
  next unless r.opponent
424
441
  opponent = @player[r.opponent]
425
442
  raise "opponent #{r.opponent} of player #{num} is not in the tournament" unless opponent
426
443
  o = opponent.find_result(r.round)
427
444
  raise "opponent #{r.opponent} of player #{num} has no result in round #{r.round}" unless o
428
- raise "opponent's result (#{o.inspect}) is not reverse of player's (#{r.inspect})" unless o.reverse.eql?(r)
445
+ score = r.rateable || o.rateable ? [] : [:score]
446
+ raise "opponent's result (#{o.inspect}) is not reverse of player's (#{r.inspect})" unless o.reverse.eql?(r, :except => score)
429
447
  end
430
448
  end
431
449
  end
@@ -587,5 +605,29 @@ module ICU
587
605
  else player.name
588
606
  end
589
607
  end
608
+
609
+ # Detect when an asymmetric result is about to be added, make the appropriate adjustment and return true.
610
+ # The conditions for an asymric result are: the player's result already exists, the opponent's result
611
+ # already exists, both results are unrateable and the reverse of one result is equal to the other, apart
612
+ # from score. In this case all we do update score of the player's result, thus allowing two results whose
613
+ # total score does not add to 1.
614
+ def add_asymmetric_result?(result)
615
+ return false if result.rateable
616
+
617
+ plr = @player[result.player]
618
+ opp = @player[result.opponent]
619
+ return false unless plr && opp
620
+
621
+ plr_result = plr.find_result(result.round)
622
+ opp_result = opp.find_result(result.round)
623
+ return false unless plr_result && opp_result
624
+ return false if plr_result.rateable || opp_result.rateable
625
+
626
+ reversed = plr_result.reverse
627
+ return false unless reversed && reversed.eql?(opp_result, :except => :score)
628
+
629
+ plr_result.score = result.score
630
+ true
631
+ end
590
632
  end
591
633
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ICU
4
4
  class Tournament
5
- VERSION = "1.4.3"
5
+ VERSION = "1.5.0"
6
6
  end
7
7
  end
data/spec/player_spec.rb CHANGED
@@ -247,6 +247,22 @@ module ICU
247
247
  end
248
248
  end
249
249
 
250
+ context "removing a result" do
251
+ before(:all) do
252
+ @p = Player.new('Mark', 'Orr', 1)
253
+ @p.add_result(Result.new(1, 1, 'W', :opponent => 37, :score => 'W', :colour => 'W'))
254
+ @p.add_result(Result.new(2, 1, 'W', :opponent => 13, :score => 'W', :colour => 'B'))
255
+ @p.add_result(Result.new(3, 1, 'W', :opponent => 7, :score => 'D', :colour => 'W'))
256
+ end
257
+
258
+ it "should find and remove a result by round number" do
259
+ result = @p.remove_result(1)
260
+ result.inspect.should == "R1P1O37WWR"
261
+ @p.results.size.should == 2
262
+ @p.results.map(&:round).join("|").should == "2|3"
263
+ end
264
+ end
265
+
250
266
  context "merge" do
251
267
  before(:each) do
252
268
  @p1 = Player.new('Mark', 'Orr', 1, :id => 1350)
data/spec/result_spec.rb CHANGED
@@ -8,7 +8,7 @@ module ICU
8
8
  lambda { Result.new(3, 5, 'W', :opponent => 11, :colour => 'W') }.should_not raise_error
9
9
  end
10
10
  end
11
-
11
+
12
12
  context "round number" do
13
13
  it "should be a positive integer" do
14
14
  lambda { Result.new(-2, 2, 0) }.should raise_error(/invalid positive integer/)
@@ -17,7 +17,7 @@ module ICU
17
17
  Result.new(' 3 ', 2, 0).round.should == 3
18
18
  end
19
19
  end
20
-
20
+
21
21
  context "player number" do
22
22
  it "should be an integer" do
23
23
  lambda { Result.new(1, ' ', 0) }.should raise_error(/invalid integer/)
@@ -27,7 +27,7 @@ module ICU
27
27
  Result.new(1, " 9 ", 0).player.should == 9
28
28
  end
29
29
  end
30
-
30
+
31
31
  context "score" do
32
32
  [1, 1.0, 'W', 'w', '+'].each do |score|
33
33
  it "should be 'W' for #{score}, #{score.class}" do
@@ -55,7 +55,7 @@ module ICU
55
55
  Result.new(1, 2, 'D').points.should == 0.5
56
56
  end
57
57
  end
58
-
58
+
59
59
  context "colour" do
60
60
  it "should be 'W' or 'B' or nil (unknown)" do
61
61
  Result.new(4, 1, 0).colour.should be_nil
@@ -66,7 +66,7 @@ module ICU
66
66
  lambda { Result.new(4, 1, 0, :colour => 'red') }.should raise_error(/invalid colour/)
67
67
  end
68
68
  end
69
-
69
+
70
70
  context "opponent number" do
71
71
  it "should be nil (the default) or an integer" do
72
72
  Result.new(4, 1, 0).opponent.should be_nil
@@ -78,27 +78,27 @@ module ICU
78
78
  Result.new(4, 1, 0, :opponent => ' 10 ').opponent.should == 10
79
79
  lambda { Result.new(4, 1, 0, :opponent => 'X') }.should raise_error(/invalid opponent number/)
80
80
  end
81
-
81
+
82
82
  it "should be different to player number" do
83
83
  lambda { Result.new(4, 1, 0, :opponent => 1) }.should raise_error(/opponent .* player .* different/)
84
84
  end
85
85
  end
86
-
86
+
87
87
  context "rateable flag" do
88
88
  it "should default to false if there is no opponent" do
89
89
  Result.new(4, 1, 0).rateable.should be_false
90
90
  end
91
-
91
+
92
92
  it "should default to true if there is an opponent" do
93
93
  Result.new(4, 1, 0, :opponent => 10).rateable.should be_true
94
94
  end
95
-
95
+
96
96
  it "should change if an opponent is added" do
97
97
  r = Result.new(4, 1, 0)
98
98
  r.opponent = 5;
99
99
  r.rateable.should be_true
100
100
  end
101
-
101
+
102
102
  it "should be settable to false from the constructor" do
103
103
  Result.new(4, 1, 0, :opponent => 10, :rateable => false).rateable.should be_false
104
104
  end
@@ -108,43 +108,43 @@ module ICU
108
108
  r.rateable= false
109
109
  r.rateable.should be_false
110
110
  end
111
-
111
+
112
112
  it "should not be settable to true if there is no opponent" do
113
113
  r = Result.new(4, 1, 0)
114
114
  r.rateable= true
115
115
  r.rateable.should be_false
116
116
  end
117
117
  end
118
-
118
+
119
119
  context "reversed result" do
120
120
  it "should be nil if there is no opponent" do
121
121
  Result.new(4, 1, 0).reverse.should be_nil
122
122
  end
123
-
123
+
124
124
  it "should have player and opponent swapped" do
125
125
  r = Result.new(4, 1, 0, :opponent => 2).reverse
126
126
  r.player.should == 2
127
127
  r.opponent.should == 1
128
128
  end
129
-
129
+
130
130
  it "should have reversed result" do
131
131
  Result.new(4, 1, 0, :opponent => 2).reverse.score.should == 'W'
132
132
  Result.new(4, 1, 1, :opponent => 2).reverse.score.should == 'L'
133
133
  Result.new(4, 1, '=', :opponent => 2).reverse.score.should == 'D'
134
134
  end
135
-
135
+
136
136
  it "should preserve rateability" do
137
137
  Result.new(4, 1, 0, :opponent => 2).reverse.rateable.should be_true
138
138
  Result.new(4, 1, 0, :opponent => 2, :rateable => false).reverse.rateable.should be_false
139
139
  end
140
140
  end
141
-
141
+
142
142
  context "renumber the player numbers in a result" do
143
143
  before(:each) do
144
144
  @r1 = Result.new(1, 4, 0)
145
145
  @r2 = Result.new(2, 3, 1, :opponent => 4, :color => 'B')
146
146
  end
147
-
147
+
148
148
  it "should renumber successfully if the map has the relevant player numbers" do
149
149
  map = { 4 => 1, 3 => 2 }
150
150
  @r1.renumber(map).player.should == 1
@@ -152,50 +152,53 @@ module ICU
152
152
  @r1.opponent.should be_nil
153
153
  @r2.opponent.should == 1
154
154
  end
155
-
155
+
156
156
  it "should raise exception if a player number is not in the map" do
157
157
  lambda { @r1.renumber({ 5 => 1, 3 => 2 }) }.should raise_error(/player.*4.*not found/)
158
158
  end
159
159
  end
160
-
161
- context "loose equality" do
160
+
161
+ context "equality" do
162
162
  before(:each) do
163
+ @r = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
163
164
  @r1 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
164
- @r2 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
165
+ @r2 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W', :rateable => false)
165
166
  @r3 = Result.new(2, 1, 0, :opponent => 2, :colour => 'W')
166
- @r4 = Result.new(1, 3, 0, :opponent => 2, :colour => 'W')
167
- @r5 = Result.new(1, 1, 1, :opponent => 2, :colour => 'W')
168
- @r6 = Result.new(1, 1, 0, :opponent => 3, :colour => 'W')
169
- @r7 = Result.new(1, 1, 0, :opponent => 2, :colour => 'B')
170
- end
171
-
172
- it "should be equal if the round, player numbers, result and colour all match" do
173
- (@r1 == @r1).should be_true
174
- (@r1 == @r2).should be_true
175
- end
176
-
177
- it "should not be equal if the round, player numbers, result or colour do not match" do
178
- (@r1 == @r3).should be_false
179
- (@r1 == @r4).should be_false
180
- (@r1 == @r5).should be_false
181
- (@r1 == @r6).should be_false
182
- (@r1 == @r7).should be_false
167
+ @r4 = Result.new(1, 1, 0, :opponent => 2, :colour => 'B')
168
+ @r5 = Result.new(2, 1, 1, :opponent => 3, :colour => 'B')
169
+ end
170
+
171
+ it "should only be equal if everything is the same" do
172
+ @r.eql?(@r).should be_true
173
+ @r.eql?(@r1).should be_true
174
+ @r.eql?(@r2).should be_false
175
+ @r.eql?(@r3).should be_false
176
+ @r.eql?(@r4).should be_false
177
+ @r.eql?(@r5).should be_false
178
+ end
179
+
180
+ it "exceptions are allowed" do
181
+ @r.eql?(@r2, :except => :rateable).should be_true
182
+ @r.eql?(@r3, :except => "round").should be_true
183
+ @r.eql?(@r4, :except => :colour).should be_true
184
+ @r.eql?(@r5, :except => [:colour, :round, :score, "opponent"]).should be_true
183
185
  end
184
186
  end
185
-
186
- context "strict equality" do
187
+
188
+ context "equality documentation example" do
187
189
  before(:each) do
188
- @r1 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
189
- @r2 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
190
- @r3 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W', :rateable => false)
191
- @r4 = Result.new(2, 1, 0, :opponent => 2, :colour => 'W')
190
+ @r = ICU::Result.new(1, 1, 'W', :opponent => 2)
191
+ @r1 = ICU::Result.new(1, 1, 'W', :opponent => 2)
192
+ @r2 = ICU::Result.new(1, 1, 'W', :opponent => 2, :rateable => false)
193
+ @r3 = ICU::Result.new(1, 1, 'L', :opponent => 2, :rateable => false)
192
194
  end
193
-
194
- it "should only be equal if everything is the same" do
195
- @r1.eql?(@r1).should be_true
196
- @r1.eql?(@r2).should be_true
197
- @r1.eql?(@r3).should be_false
198
- @r1.eql?(@r4).should be_false
195
+
196
+ it "should be correct" do
197
+ @r.eql?(@r1).should be_true
198
+ @r.eql?(@r2).should be_false
199
+ @r.eql?(@r3).should be_false
200
+ @r.eql?(@r2, :except => :rateable).should be_true
201
+ @r.eql?(@r3, :except => [:rateable, :score]).should be_true
199
202
  end
200
203
  end
201
204
  end
@@ -32,8 +32,9 @@ module ICU
32
32
  042 2009-11-09
33
33
  001 10 Fischer,Bobby 1.5 1 30 w = 20 b 1
34
34
  001 20 Kasparov,Garry 1.0 2 30 b 1 10 w 0
35
- 001 30 Orr,Mark 0.5 3 10 b = 20 w 0
35
+ 001 30 Orr,Mark 0.5 3 10 b = 20 w 0 #
36
36
  EOS
37
+ @s.sub!(/#/, '')
37
38
  end
38
39
 
39
40
  it "should serialize to Krause" do
@@ -324,7 +325,7 @@ EOS
324
325
  it "can be added one at a time" do
325
326
  @t.add_result(Result.new(1, 1, 'W', :opponent => 2))
326
327
  @t.add_result(Result.new(2, 2, 'D', :opponent => 3))
327
- @t.add_result(Result.new(3, 3, 'L', :opponent => 1))
328
+ @t.add_result(Result.new(3, 3, 'L', :opponent => 1, :rateable => false))
328
329
  @mark.results.size.should == 2
329
330
  @mark.points.should == 2.0
330
331
  @gary.results.size.should == 2
@@ -333,10 +334,11 @@ EOS
333
334
  @boby.points.should == 0.5
334
335
  end
335
336
 
336
- it "asymmetric results cannot be added" do
337
+ it "results with asymmetric scores cannot be added unless both results are unrateable" do
337
338
  @t.add_result(Result.new(1, 1, 'W', :opponent => 2))
338
339
  lambda { @t.add_result(Result.new(1, 2, 'D', :opponent => 1)) }.should raise_error(/result.*match/)
339
340
  lambda { @t.add_result(Result.new(1, 2, 'L', :opponent => 1, :rateable => false)) }.should raise_error(/result.*match/)
341
+ lambda { @t.add_result(Result.new(3, 3, 'L', :opponent => 1, :rateable => false)) }.should_not raise_error
340
342
  end
341
343
 
342
344
  it "should have a defined player" do
@@ -350,6 +352,13 @@ EOS
350
352
  it "should be consistent with the tournament's number of rounds" do
351
353
  lambda { @t.add_result(Result.new(4, 1, 'W', :opponent => 2)) }.should raise_error(/round/)
352
354
  end
355
+
356
+ it "documentation example should ne correct" do
357
+ @t.add_result(ICU::Result.new(3, 2, 'L', :opponent => 1, :rateable => false))
358
+ @t.add_result(ICU::Result.new(3, 1, 'L', :opponent => 2, :rateable => false))
359
+ @t.player(1).results.first.points.should == 0.0
360
+ @t.player(2).results.first.points.should == 0.0
361
+ end
353
362
  end
354
363
 
355
364
  context "finding players" do
@@ -495,6 +504,26 @@ EOS
495
504
  @t.player(2).id = 1350
496
505
  @t.invalid.should match(/duplicate.*ICU/)
497
506
  end
507
+
508
+ it "should allow players to have no results" do
509
+ (1..3).each { |r| @t.player(1).remove_result(r) }
510
+ @t.invalid.should be_false
511
+ end
512
+
513
+ it "should not allow asymmetric scores for rateable results" do
514
+ @t.player(1).find_result(1).score = 'L'
515
+ @t.invalid.should match(/result.*reverse/)
516
+ end
517
+
518
+ it "should allow asymmetric scores for unrateable results" do
519
+ @t.player(1).find_result(1).score = 'L'
520
+ (1..2).each do |p|
521
+ r = @t.player(p).find_result(1)
522
+ r.rateable = false
523
+ r.score = 'L'
524
+ end
525
+ @t.invalid.should be_false
526
+ end
498
527
  end
499
528
 
500
529
  context "renumbering" do
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: icu_tournament
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 1.4.3
5
+ version: 1.5.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Mark Orr
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-06-29 00:00:00 Z
13
+ date: 2011-07-01 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: dbf
@@ -172,7 +172,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
172
172
  requirements:
173
173
  - - ">="
174
174
  - !ruby/object:Gem::Version
175
- hash: 1676868114987793590
175
+ hash: 3338096865289385673
176
176
  segments:
177
177
  - 0
178
178
  version: "0"
@@ -181,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
181
  requirements:
182
182
  - - ">="
183
183
  - !ruby/object:Gem::Version
184
- hash: 1676868114987793590
184
+ hash: 3338096865289385673
185
185
  segments:
186
186
  - 0
187
187
  version: "0"