1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# frozen_string_literal: true

# Port of https://github.com/philihp/openskill.js/blob/main/src/models/plackett-luce.ts

module OpenSkill
  class PlackettLuce
    #: (Model model) -> void
    def initialize(model)
      @beta_sq = model.beta_sq
      @epsilon = model.epsilon
    end

    #: (Game game) -> Array[Array[Rating]]
    def rate(game)
      return game.teams if game.teams.length <= 1

      team_ratings = game.team_ratings
      c = Util.util_c(team_ratings, @beta_sq)
      sum_q = Util.util_sum_q(team_ratings, c)
      a = Util.util_a(team_ratings)

      team_ratings.map.with_index { |tr, i|
        mu_over_ce = Math.exp(tr.mu / c)

        omega_sum = 0.0
        delta_sum = 0.0

        team_ratings.each.with_index
          .select {|qr, _| qr.rank <= tr.rank }
          .each do |_, q|
            quotient = mu_over_ce / sum_q[q]
            omega_sum += ((i == q ? 1 : 0) - quotient) / a[q]
            delta_sum += (quotient * (1 - quotient)) / a[q]
          end

        g = Util.gamma(c, team_ratings.length, tr)
        omega = omega_sum * (tr.sigma_sq / c)
        delta = g * delta_sum * (tr.sigma_sq / c**2)

        tr.team.map { |player|
          share = player.sigma**2 / tr.sigma_sq
          new_mu = player.mu + share * omega
          new_sigma = player.sigma * Math.sqrt([1 - share * delta, @epsilon].max)
          Rating.new(new_mu, new_sigma)
        }
      }
    end
  end
end