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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# frozen_string_literal: true
module OpenSkill
# A game result: placements (first to last), each containing one or more
# tied teams, each containing one or more player Ratings.
#
# Game.new([[winner]], [[loser]])
# Game.new([[first]], [[tied_a], [tied_b]], [[last]])
#
# Each argument is a placement group — an array of teams that tied at
# that position. Each team is an array of Rating objects.
# Intermediate structure for team-level aggregation during rating.
# Produced by Game#team_ratings, consumed by PlackettLuce and Util helpers.
# Signed in sig/open_skill/rating.rbs: a Data class.
# @rbs skip
TeamRating = Data.define(:mu, :sigma_sq, :team, :rank)
# Reopened rather than defined in the block above, so the inline
# parser reads its type.
class TeamRating
#: () -> [Float, Float, Array[Rating], Integer]
def to_a = [mu, sigma_sq, team, rank]
end
class Game
attr_reader :rank_groups #: Array[Array[Array[Rating]]]
attr_reader :teams #: Array[Array[Rating]]
attr_reader :rank #: Array[Integer]
#: (*Array[Array[Rating]] rank_groups) -> void
def initialize(*rank_groups)
@rank_groups = rank_groups.freeze
@teams = @rank_groups.flatten(1).freeze
@rank = @rank_groups.flat_map.with_index { |group, i| [i] * group.size }.freeze
freeze
end
#: (Array[Rating] winner, Array[Rating] loser) -> Game
def self.win_lose(winner, loser)
new([winner], [loser])
end
#: () -> Array[TeamRating]
def team_ratings
teams.zip(rankings).map { |team, rank|
TeamRating.new(
mu: team.sum { |p| p.mu }, #: Float
sigma_sq: team.sum { |p| p.sigma**2 }, #: Float
team: team,
rank: rank, #: Integer
)
}
end
private
# Dense ranks: tied teams share a rank, next group gets the next integer.
# [0, 0, 1] not [0, 0, 2]. The Plackett-Luce math only uses relative
# comparisons (<=, >=, ==) so dense and sparse ranks are equivalent.
# The prior implementation produced sparse/positional ranks where gaps
# reflect the number of tied teams ([0, 0, 2]). Restore that if a
# future model needs it.
#: () -> Array[Integer]
def rankings
rank.chunk_while { |a, b| a == b }.flat_map.with_index { |group, i|
[i] * group.size
}
end
end
end