openskill 0.1.0 → 1.0.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.
@@ -0,0 +1,610 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative '../statistics/normal'
5
+ require_relative 'common'
6
+
7
+ module OpenSkill
8
+ module Models
9
+ # Thurstone-Mosteller Full rating model (Based on Algorithm 3 with partial pairing)
10
+ #
11
+ # This model uses partial pairing with a sliding window for efficiency.
12
+ # It uses a maximum likelihood estimation approach for rating estimation.
13
+ class ThurstoneMostellerPart
14
+ attr_reader :mu, :sigma, :beta, :kappa, :tau, :margin, :limit_sigma, :balance, :gamma, :epsilon, :window_size
15
+
16
+ # Default gamma function for ThurstoneMostellerPart
17
+ DEFAULT_GAMMA = lambda do |_c, _k, _mu, sigma_squared, _team, _rank, _weights|
18
+ Math.sqrt(sigma_squared) / _c
19
+ end
20
+
21
+ # @param mu [Float] initial mean skill rating
22
+ # @param sigma [Float] initial standard deviation
23
+ # @param beta [Float] performance uncertainty
24
+ # @param kappa [Float] minimum variance (regularization)
25
+ # @param gamma [Proc] custom gamma function
26
+ # @param tau [Float] dynamics factor (skill decay)
27
+ # @param epsilon [Float] draw margin for Thurstone-Mosteller
28
+ # @param margin [Float] score margin for impressive wins
29
+ # @param limit_sigma [Boolean] prevent sigma from increasing
30
+ # @param balance [Boolean] emphasize rating outliers
31
+ # @param window_size [Integer] sliding window size for partial pairing
32
+ def initialize(
33
+ mu: 25.0,
34
+ sigma: 25.0 / 3.0,
35
+ beta: 25.0 / 6.0,
36
+ kappa: 0.0001,
37
+ gamma: DEFAULT_GAMMA,
38
+ tau: 25.0 / 300.0,
39
+ epsilon: 0.1,
40
+ margin: 0.0,
41
+ limit_sigma: false,
42
+ balance: false,
43
+ window_size: 4
44
+ )
45
+ @mu = mu.to_f
46
+ @sigma = sigma.to_f
47
+ @beta = beta.to_f
48
+ @kappa = kappa.to_f
49
+ @gamma = gamma
50
+ @tau = tau.to_f
51
+ @epsilon = epsilon.to_f
52
+ @margin = margin.to_f
53
+ @limit_sigma = limit_sigma
54
+ @balance = balance
55
+ @window_size = window_size.to_i
56
+ end
57
+
58
+ # Create a new rating with default or custom parameters
59
+ #
60
+ # @param mu [Float, nil] override default mu
61
+ # @param sigma [Float, nil] override default sigma
62
+ # @param name [String, nil] optional player name
63
+ # @return [Rating] a new rating object
64
+ def create_rating(mu: nil, sigma: nil, name: nil)
65
+ Rating.new(
66
+ mu: mu || @mu,
67
+ sigma: sigma || @sigma,
68
+ name: name
69
+ )
70
+ end
71
+
72
+ # Load a rating from an array [mu, sigma]
73
+ #
74
+ # @param rating_array [Array<Numeric>] [mu, sigma]
75
+ # @param name [String, nil] optional player name
76
+ # @return [Rating] a new rating object
77
+ # @raise [ArgumentError] if rating_array is invalid
78
+ def load_rating(rating_array, name: nil)
79
+ raise ArgumentError, "Rating must be an Array, got #{rating_array.class}" unless rating_array.is_a?(Array)
80
+ raise ArgumentError, 'Rating array must have exactly 2 elements' unless rating_array.size == 2
81
+ raise ArgumentError, 'Rating values must be numeric' unless rating_array.all? { |v| v.is_a?(Numeric) }
82
+
83
+ Rating.new(mu: rating_array[0], sigma: rating_array[1], name: name)
84
+ end
85
+
86
+ # Calculate new ratings after a match
87
+ #
88
+ # @param teams [Array<Array<Rating>>] list of teams
89
+ # @param ranks [Array<Numeric>, nil] team ranks (lower is better, 0-indexed)
90
+ # @param scores [Array<Numeric>, nil] team scores (higher is better)
91
+ # @param weights [Array<Array<Numeric>>, nil] player contribution weights
92
+ # @param tau [Float, nil] override tau for this match
93
+ # @param limit_sigma [Boolean, nil] override limit_sigma for this match
94
+ # @return [Array<Array<Rating>>] updated teams
95
+ def calculate_ratings(teams, ranks: nil, scores: nil, weights: nil, tau: nil, limit_sigma: nil)
96
+ validate_teams!(teams)
97
+ validate_ranks!(teams, ranks) if ranks
98
+ validate_scores!(teams, scores) if scores
99
+ validate_weights!(teams, weights) if weights
100
+
101
+ raise ArgumentError, 'Cannot provide both ranks and scores' if ranks && scores
102
+
103
+ # Deep copy teams to avoid mutating input
104
+ original_teams = teams
105
+ teams = deep_copy_teams(teams)
106
+
107
+ # Apply tau (skill decay over time)
108
+ tau_value = tau || @tau
109
+ tau_squared = tau_value**2
110
+ teams.each do |team|
111
+ team.each do |player|
112
+ player.sigma = Math.sqrt(player.sigma**2 + tau_squared)
113
+ end
114
+ end
115
+
116
+ # Convert scores to ranks if provided
117
+ if !ranks && scores
118
+ ranks = scores.map { |s| -s }
119
+ ranks = calculate_rankings(teams, ranks)
120
+ end
121
+
122
+ # Normalize weights to [1, 2] range
123
+ weights = weights.map { |w| Common.normalize(w, 1, 2) } if weights
124
+
125
+ # Sort teams by rank and track original order
126
+ tenet = nil
127
+ if ranks
128
+ sorted_objects, restoration_indices = Common.unwind(ranks, teams)
129
+ teams = sorted_objects
130
+ tenet = restoration_indices
131
+
132
+ weights, = Common.unwind(ranks, weights) if weights
133
+
134
+ ranks = ranks.sort
135
+ end
136
+
137
+ # Compute new ratings
138
+ result = compute_ratings(teams, ranks: ranks, scores: scores, weights: weights)
139
+
140
+ # Restore original order
141
+ result, = Common.unwind(tenet, result) if ranks && tenet
142
+
143
+ # Apply sigma limiting if requested
144
+ limit_sigma_value = limit_sigma.nil? ? @limit_sigma : limit_sigma
145
+ if limit_sigma_value
146
+ result = result.each_with_index.map do |team, team_idx|
147
+ team.each_with_index.map do |player, player_idx|
148
+ player.sigma = [player.sigma, original_teams[team_idx][player_idx].sigma].min
149
+ player
150
+ end
151
+ end
152
+ end
153
+
154
+ result
155
+ end
156
+
157
+ # Predict win probability for each team
158
+ #
159
+ # @param teams [Array<Array<Rating>>] list of teams
160
+ # @return [Array<Float>] probability each team wins
161
+ def predict_win_probability(teams)
162
+ validate_teams!(teams)
163
+
164
+ n = teams.size
165
+
166
+ # Special case for 2 teams
167
+ if n == 2
168
+ team_ratings = calculate_team_ratings(teams)
169
+ a = team_ratings[0]
170
+ b = team_ratings[1]
171
+
172
+ result = phi_major(
173
+ (a.mu - b.mu) / Math.sqrt(2 * @beta**2 + a.sigma_squared + b.sigma_squared)
174
+ )
175
+ return [result, 1 - result]
176
+ end
177
+
178
+ # For n teams, compute pairwise probabilities
179
+ team_ratings = teams.map { |team| calculate_team_ratings([team])[0] }
180
+
181
+ win_probs = []
182
+ team_ratings.each_with_index do |team_i, i|
183
+ prob_sum = 0.0
184
+ team_ratings.each_with_index do |team_j, j|
185
+ next if i == j
186
+
187
+ prob_sum += phi_major(
188
+ (team_i.mu - team_j.mu) / Math.sqrt(2 * @beta**2 + team_i.sigma_squared + team_j.sigma_squared)
189
+ )
190
+ end
191
+ win_probs << prob_sum / (n - 1)
192
+ end
193
+
194
+ # Normalize to sum to 1
195
+ total = win_probs.sum
196
+ win_probs.map { |p| p / total }
197
+ end
198
+
199
+ # Predict draw probability
200
+ #
201
+ # @param teams [Array<Array<Rating>>] list of teams
202
+ # @return [Float] probability of a draw
203
+ def predict_draw_probability(teams)
204
+ validate_teams!(teams)
205
+
206
+ total_player_count = teams.sum(&:size)
207
+ draw_probability = 1.0 / total_player_count
208
+ draw_margin = Math.sqrt(total_player_count) * @beta * phi_major_inverse((1 + draw_probability) / 2)
209
+
210
+ pairwise_probs = []
211
+ teams.combination(2).each do |team_a, team_b|
212
+ team_a_ratings = calculate_team_ratings([team_a])
213
+ team_b_ratings = calculate_team_ratings([team_b])
214
+
215
+ mu_a = team_a_ratings[0].mu
216
+ sigma_a = team_a_ratings[0].sigma_squared
217
+ mu_b = team_b_ratings[0].mu
218
+ sigma_b = team_b_ratings[0].sigma_squared
219
+
220
+ denominator = Math.sqrt(2 * @beta**2 + sigma_a + sigma_b)
221
+
222
+ pairwise_probs << (
223
+ phi_major((draw_margin - mu_a + mu_b) / denominator) -
224
+ phi_major((mu_b - mu_a - draw_margin) / denominator)
225
+ )
226
+ end
227
+
228
+ pairwise_probs.sum / pairwise_probs.size
229
+ end
230
+
231
+ # Predict rank probability for each team
232
+ #
233
+ # @param teams [Array<Array<Rating>>] list of teams
234
+ # @return [Array<Array(Integer, Float)>] rank and probability for each team
235
+ def predict_rank_probability(teams)
236
+ validate_teams!(teams)
237
+
238
+ n = teams.size
239
+ team_ratings = calculate_team_ratings(teams)
240
+
241
+ # Calculate win probability for each team against all others
242
+ win_probs = team_ratings.map do |team_i|
243
+ prob = 0.0
244
+ team_ratings.each do |team_j|
245
+ next if team_i == team_j
246
+
247
+ prob += phi_major(
248
+ (team_i.mu - team_j.mu) /
249
+ Math.sqrt(2 * @beta**2 + team_i.sigma_squared + team_j.sigma_squared)
250
+ )
251
+ end
252
+ prob / (n - 1)
253
+ end
254
+
255
+ # Normalize probabilities
256
+ total = win_probs.sum
257
+ normalized_probs = win_probs.map { |p| p / total }
258
+
259
+ # Sort by probability (descending) and assign ranks
260
+ sorted_indices = normalized_probs.each_with_index.sort_by { |prob, _| -prob }
261
+ ranks = Array.new(n)
262
+
263
+ current_rank = 1
264
+ sorted_indices.each_with_index do |(prob, team_idx), i|
265
+ current_rank = i + 1 if i > 0 && prob < sorted_indices[i - 1][0]
266
+ ranks[team_idx] = current_rank
267
+ end
268
+
269
+ ranks.zip(normalized_probs)
270
+ end
271
+
272
+ private
273
+
274
+ # Helper for log(1 + x)
275
+ def log1p(value)
276
+ Math.log(1 + value)
277
+ end
278
+
279
+ # Rating class for individual players
280
+ class Rating
281
+ attr_accessor :mu, :sigma, :name
282
+ attr_reader :id
283
+
284
+ def initialize(mu:, sigma:, name: nil)
285
+ @id = SecureRandom.hex
286
+ @mu = mu.to_f
287
+ @sigma = sigma.to_f
288
+ @name = name
289
+ end
290
+
291
+ # Calculate display rating (conservative estimate)
292
+ #
293
+ # @param z [Float] number of standard deviations
294
+ # @param alpha [Float] scaling factor
295
+ # @param target [Float] target adjustment
296
+ # @return [Float] the ordinal rating
297
+ def ordinal(z: 3.0, alpha: 1.0, target: 0.0)
298
+ alpha * ((@mu - z * @sigma) + (target / alpha))
299
+ end
300
+
301
+ def <=>(other)
302
+ return nil unless other.is_a?(Rating)
303
+
304
+ ordinal <=> other.ordinal
305
+ end
306
+
307
+ def <(other)
308
+ raise ArgumentError, 'comparison with non-Rating' unless other.is_a?(Rating)
309
+
310
+ ordinal < other.ordinal
311
+ end
312
+
313
+ def >(other)
314
+ raise ArgumentError, 'comparison with non-Rating' unless other.is_a?(Rating)
315
+
316
+ ordinal > other.ordinal
317
+ end
318
+
319
+ def <=(other)
320
+ raise ArgumentError, 'comparison with non-Rating' unless other.is_a?(Rating)
321
+
322
+ ordinal <= other.ordinal
323
+ end
324
+
325
+ def >=(other)
326
+ raise ArgumentError, 'comparison with non-Rating' unless other.is_a?(Rating)
327
+
328
+ ordinal >= other.ordinal
329
+ end
330
+
331
+ def ==(other)
332
+ return false unless other.is_a?(Rating)
333
+
334
+ @mu == other.mu && @sigma == other.sigma
335
+ end
336
+
337
+ def hash
338
+ [@id, @mu, @sigma].hash
339
+ end
340
+
341
+ def eql?(other)
342
+ self == other
343
+ end
344
+
345
+ def to_s
346
+ "Rating(mu=#{@mu}, sigma=#{@sigma}#{", name=#{@name}" if @name})"
347
+ end
348
+
349
+ def inspect
350
+ to_s
351
+ end
352
+ end
353
+
354
+ # Internal class for team ratings
355
+ class TeamRating
356
+ attr_reader :mu, :sigma_squared, :team, :rank
357
+
358
+ def initialize(mu:, sigma_squared:, team:, rank:)
359
+ @mu = mu.to_f
360
+ @sigma_squared = sigma_squared.to_f
361
+ @team = team
362
+ @rank = rank.to_i
363
+ end
364
+
365
+ def ==(other)
366
+ return false unless other.is_a?(TeamRating)
367
+
368
+ @mu == other.mu &&
369
+ @sigma_squared == other.sigma_squared &&
370
+ @team == other.team &&
371
+ @rank == other.rank
372
+ end
373
+
374
+ def hash
375
+ [@mu, @sigma_squared, @team, @rank].hash
376
+ end
377
+
378
+ def to_s
379
+ "TeamRating(mu=#{@mu}, sigma_squared=#{@sigma_squared}, rank=#{@rank})"
380
+ end
381
+ end
382
+
383
+ # Validate teams structure
384
+ def validate_teams!(teams)
385
+ raise ArgumentError, 'Teams must be an Array' unless teams.is_a?(Array)
386
+ raise ArgumentError, 'Must have at least 2 teams' if teams.size < 2
387
+
388
+ teams.each_with_index do |team, idx|
389
+ raise ArgumentError, "Team #{idx} must be an Array" unless team.is_a?(Array)
390
+ raise ArgumentError, "Team #{idx} must have at least 1 player" if team.empty?
391
+
392
+ team.each do |player|
393
+ raise ArgumentError, "All players must be Rating objects, got #{player.class}" unless player.is_a?(Rating)
394
+ end
395
+ end
396
+ end
397
+
398
+ # Validate ranks
399
+ def validate_ranks!(teams, ranks)
400
+ raise ArgumentError, "Ranks must be an Array, got #{ranks.class}" unless ranks.is_a?(Array)
401
+ raise ArgumentError, 'Ranks must have same length as teams' if ranks.size != teams.size
402
+
403
+ ranks.each do |rank|
404
+ raise ArgumentError, "All ranks must be numeric, got #{rank.class}" unless rank.is_a?(Numeric)
405
+ end
406
+ end
407
+
408
+ # Validate scores
409
+ def validate_scores!(teams, scores)
410
+ raise ArgumentError, "Scores must be an Array, got #{scores.class}" unless scores.is_a?(Array)
411
+ raise ArgumentError, 'Scores must have same length as teams' if scores.size != teams.size
412
+
413
+ scores.each do |score|
414
+ raise ArgumentError, "All scores must be numeric, got #{score.class}" unless score.is_a?(Numeric)
415
+ end
416
+ end
417
+
418
+ # Validate weights
419
+ def validate_weights!(teams, weights)
420
+ raise ArgumentError, "Weights must be an Array, got #{weights.class}" unless weights.is_a?(Array)
421
+ raise ArgumentError, 'Weights must have same length as teams' if weights.size != teams.size
422
+
423
+ weights.each_with_index do |team_weights, idx|
424
+ raise ArgumentError, "Weights for team #{idx} must be an Array" unless team_weights.is_a?(Array)
425
+ unless team_weights.size == teams[idx].size
426
+ raise ArgumentError, "Weights for team #{idx} must match team size"
427
+ end
428
+
429
+ team_weights.each do |weight|
430
+ raise ArgumentError, "All weights must be numeric, got #{weight.class}" unless weight.is_a?(Numeric)
431
+ end
432
+ end
433
+ end
434
+
435
+ # Deep copy teams to avoid mutation
436
+ def deep_copy_teams(teams)
437
+ teams.map do |team|
438
+ team.map do |player|
439
+ Rating.new(mu: player.mu, sigma: player.sigma, name: player.name).tap do |new_player|
440
+ new_player.instance_variable_set(:@id, player.id)
441
+ end
442
+ end
443
+ end
444
+ end
445
+
446
+ # Calculate team ratings from individual player ratings
447
+ def calculate_team_ratings(game, ranks: nil, weights: nil)
448
+ ranks ||= calculate_rankings(game)
449
+
450
+ game.each_with_index.map do |team, idx|
451
+ sorted_team = team.sort_by { |p| -p.ordinal }
452
+ max_ordinal = sorted_team.first.ordinal
453
+
454
+ mu_sum = 0.0
455
+ sigma_squared_sum = 0.0
456
+
457
+ sorted_team.each do |player|
458
+ balance_weight = if @balance
459
+ ordinal_diff = max_ordinal - player.ordinal
460
+ 1 + (ordinal_diff / (max_ordinal + @kappa))
461
+ else
462
+ 1.0
463
+ end
464
+
465
+ mu_sum += player.mu * balance_weight
466
+ sigma_squared_sum += (player.sigma * balance_weight)**2
467
+ end
468
+
469
+ TeamRating.new(
470
+ mu: mu_sum,
471
+ sigma_squared: sigma_squared_sum,
472
+ team: team,
473
+ rank: ranks[idx].to_i
474
+ )
475
+ end
476
+ end
477
+
478
+ # Calculate rankings from scores or indices
479
+ def calculate_rankings(game, ranks = nil)
480
+ return [] if game.empty?
481
+
482
+ team_scores = if ranks
483
+ ranks.each_with_index.map { |rank, idx| rank || idx }
484
+ else
485
+ game.each_index.to_a
486
+ end
487
+
488
+ sorted_scores = team_scores.sort
489
+ rank_map = {}
490
+ sorted_scores.each_with_index do |value, index|
491
+ rank_map[value] ||= index
492
+ end
493
+
494
+ team_scores.map { |score| rank_map[score].to_f }
495
+ end
496
+
497
+ # Core rating computation algorithm using Thurstone-Mosteller Partial pairing
498
+ def compute_ratings(teams, ranks: nil, scores: nil, weights: nil)
499
+ team_ratings = calculate_team_ratings(teams, ranks: ranks)
500
+ num_teams = team_ratings.size
501
+
502
+ # Build score mapping for margin calculations
503
+ score_mapping = {}
504
+ if scores && scores.size == team_ratings.size
505
+ team_ratings.each_with_index do |_, i|
506
+ score_mapping[i] = scores[i]
507
+ end
508
+ end
509
+
510
+ team_ratings.each_with_index.map do |team_i, i|
511
+ omega_sum = 0.0
512
+ delta_sum = 0.0
513
+ comparisons = 0
514
+
515
+ # Thurstone-Mosteller Part: Use sliding window for partial pairing
516
+ start_idx = [0, i - @window_size].max
517
+ end_idx = [num_teams, i + @window_size + 1].min
518
+
519
+ (start_idx...end_idx).each do |q|
520
+ next if q == i
521
+
522
+ team_q = team_ratings[q]
523
+
524
+ # Calculate c_iq with 2x factor for partial pairing
525
+ c_iq = 2 * Math.sqrt(team_i.sigma_squared + team_q.sigma_squared + 2 * @beta**2)
526
+
527
+ # Calculate margin factor
528
+ margin_factor = 1.0
529
+ if scores && score_mapping.key?(i) && score_mapping.key?(q)
530
+ score_diff = (score_mapping[i] - score_mapping[q]).abs
531
+ margin_factor = log1p(score_diff / @margin) if score_diff > @margin && @margin > 0.0
532
+ end
533
+
534
+ # Calculate delta_mu
535
+ delta_mu = ((team_i.mu - team_q.mu) / c_iq) * margin_factor
536
+ epsilon_over_c = @epsilon / c_iq
537
+
538
+ # Use v/w/vt/wt functions based on rank comparison
539
+ if team_q.rank > team_i.rank
540
+ # team_i won
541
+ omega_sum += (team_i.sigma_squared / c_iq) * Common.v(delta_mu, epsilon_over_c)
542
+ v_val = Common.w(delta_mu, epsilon_over_c)
543
+ elsif team_q.rank < team_i.rank
544
+ # team_i lost
545
+ omega_sum -= (team_i.sigma_squared / c_iq) * Common.v(-delta_mu, epsilon_over_c)
546
+ v_val = Common.w(-delta_mu, epsilon_over_c)
547
+ else
548
+ # draw
549
+ omega_sum += (team_i.sigma_squared / c_iq) * Common.vt(delta_mu, epsilon_over_c)
550
+ v_val = Common.wt(delta_mu, epsilon_over_c)
551
+ end
552
+
553
+ # Apply gamma
554
+ team_weights = weights ? weights[i] : nil
555
+ gamma_value = @gamma.call(
556
+ c_iq,
557
+ team_ratings.size,
558
+ team_i.mu,
559
+ team_i.sigma_squared,
560
+ team_i.team,
561
+ team_i.rank,
562
+ team_weights
563
+ )
564
+
565
+ delta_sum += (gamma_value * team_i.sigma_squared / (c_iq**2)) * v_val
566
+ comparisons += 1
567
+ end
568
+
569
+ # Average over comparisons
570
+ omega = comparisons > 0 ? omega_sum / comparisons : 0.0
571
+ delta = comparisons > 0 ? delta_sum / comparisons : 0.0
572
+
573
+ # Update each player in the team
574
+ team_i.team.each_with_index.map do |player, j|
575
+ weight = weights ? weights[i][j] : 1.0
576
+
577
+ new_mu = player.mu
578
+ new_sigma = player.sigma
579
+
580
+ if omega >= 0
581
+ new_mu += (new_sigma**2 / team_i.sigma_squared) * omega * weight
582
+ new_sigma *= Math.sqrt(
583
+ [1 - (new_sigma**2 / team_i.sigma_squared) * delta * weight, @kappa].max
584
+ )
585
+ else
586
+ new_mu += (new_sigma**2 / team_i.sigma_squared) * omega / weight
587
+ new_sigma *= Math.sqrt(
588
+ [1 - (new_sigma**2 / team_i.sigma_squared) * delta / weight, @kappa].max
589
+ )
590
+ end
591
+
592
+ player.mu = new_mu
593
+ player.sigma = new_sigma
594
+ player
595
+ end
596
+ end
597
+ end
598
+
599
+ # Normal distribution CDF
600
+ def phi_major(value)
601
+ Statistics::Normal.cdf(value)
602
+ end
603
+
604
+ # Normal distribution inverse CDF
605
+ def phi_major_inverse(value)
606
+ Statistics::Normal.inv_cdf(value)
607
+ end
608
+ end
609
+ end
610
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenSkill
4
- VERSION = '0.1.0'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/openskill.rb CHANGED
@@ -4,6 +4,10 @@ require_relative 'openskill/version'
4
4
  require_relative 'openskill/statistics/normal'
5
5
  require_relative 'openskill/models/common'
6
6
  require_relative 'openskill/models/plackett_luce'
7
+ require_relative 'openskill/models/bradley_terry_full'
8
+ require_relative 'openskill/models/bradley_terry_part'
9
+ require_relative 'openskill/models/thurstone_mosteller_full'
10
+ require_relative 'openskill/models/thurstone_mosteller_part'
7
11
 
8
12
  module OpenSkill
9
13
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openskill
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tamas Erdos
@@ -90,8 +90,12 @@ files:
90
90
  - LICENSE
91
91
  - README.md
92
92
  - lib/openskill.rb
93
+ - lib/openskill/models/bradley_terry_full.rb
94
+ - lib/openskill/models/bradley_terry_part.rb
93
95
  - lib/openskill/models/common.rb
94
96
  - lib/openskill/models/plackett_luce.rb
97
+ - lib/openskill/models/thurstone_mosteller_full.rb
98
+ - lib/openskill/models/thurstone_mosteller_part.rb
95
99
  - lib/openskill/statistics/normal.rb
96
100
  - lib/openskill/version.rb
97
101
  homepage: https://github.com/erdostom/openskill-ruby