summaryrefslogtreecommitdiff
path: root/rater.py
blob: 2d07e038df996b6b2275158341e15e7bde4dc6c5 (plain)
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/python3
import os
import sys
import logging

import trueskill
import mpmath
mpmath.mp.dps = 50  # increase if FloatingPointError

UNKNOWN_PLAYER_SIGMA = int(os.environ.get("UNKNOWN_PLAYER_SIGMA") or 500)
TAU = float(os.environ.get("TAU") or 1000/100.0)

# mapping from Tier (-1 - 30) to average skill tier points
vst_points = {
    -1: 1,
    0: 1
}
for c in range(1, 12):
    vst_points[c] = (109 + 1/11) * (c + 0.5)
for c in range(1, 5):
    vst_points[11 + c] = vst_points[11] + 50 * (c + 0.5)
for c in range(1, 10):
    vst_points[15 + c] = vst_points[15] + (66 + 2/3) * (c + 0.5)
for c in range(1, 4):
    vst_points[24 + c] = vst_points[24] + (133 + 1/3) * (c + 0.5)
for c in range(1, 3):
    vst_points[27 + c] = vst_points[27] + 200 * (c + 0.5)
# ---

env = trueskill.TrueSkill(
    backend="mpmath",
    mu=1500,
    sigma=1000,
    beta=10.0/30*3000,
    tau=TAU,
    draw_probability=0
)

"""
Return a (mu, sigma) based on information known about a player.
"""
def get_trueskill_seed(player):
    # fallback 1 - approximate by max(ranked, blitz) points
    rank_points = None
    if player.rank_points_ranked is not None and player.rank_points_ranked != 0:
        rank_points = player.rank_points_ranked
    if player.rank_points_blitz is not None and player.rank_points_blitz != 0:
        if rank_points is None:
            rank_points = player.rank_points_blitz
        else:
            if player.rank_points_blitz > rank_points:
                rank_points = player.rank_points_blitz

    if rank_points is not None:
        sigma = UNKNOWN_PLAYER_SIGMA * (2.0/3.0)  # more accurate than skill tier = more trust
        mu = float(rank_points) + sigma
    else:
        # fallback 2 - approximate rank points by skill tier
        sigma = UNKNOWN_PLAYER_SIGMA
        mu = vst_points[player.skill_tier] + sigma

    return (mu, sigma)


"""
Mutate a match structure by updating TrueSkill values.
Returns changed match object.
"""
def rate_match(match):
    trueskill_column = None  # on player
    if match.game_mode == "casual":  # mode names mapped by processor!!!
        trueskill_column = "trueskill_casual"
    if match.game_mode == "ranked":
        trueskill_column = "trueskill_ranked"
    if match.game_mode == "blitz":
        trueskill_column = "trueskill_blitz"
    if match.game_mode == "br":
        trueskill_column = "trueskill_br"
    if trueskill_column is None:
        logger.info("got unsupported game mode %s", match.game_mode)
        return

    matchup_shared = []  # trueskill shared across all modes
    matchup = []

    anyAfk = False
    if len(match.rosters) != 2 \
            or len(match.rosters[0].participants) != 3 \
            or len(match.rosters[1].participants) != 3:
        logger.error("got an invalid matchup %s", match.api_id)
        anyAfk = True

    for participant in match.participants:
        participant.participant_items[0].any_afk = False
        if participant.went_afk == 1:
            logger.info("got an afk matchup %s", match.api_id)
            anyAfk = True
            break

    if anyAfk:
        match.trueskill_quality = 0
        for participant in match.participants:
            participant.participant_items[0].any_afk = True
        return

    for roster in match.rosters:
        team_shared = []
        team = []
        for participant in roster.participants:
            player = participant.player[0]

            # calculate TrueSkill shared across all modes = starting point for all modes
            if player.trueskill_mu is not None:
                mu_shared = player.trueskill_mu
                sigma_shared = player.trueskill_sigma
            else:
                mu_shared, sigma_shared = get_trueskill_seed(player)

            team_shared.append(env.create_rating(float(mu_shared), float(sigma_shared)))

            # calculate queue specific TrueSkill
            if getattr(player, trueskill_column + "_mu") is not None:
                sigma = getattr(player, trueskill_column + "_sigma")
                mu = getattr(player, trueskill_column + "_mu")
            else:
                # fallback 1 - approximate by shared trueskill
                sigma = sigma_shared
                mu = mu_shared

            team.append(env.create_rating(float(mu), float(sigma)))


        matchup_shared.append(team_shared)
        matchup.append(team)

    logger.info("got a valid matchup %s", match.api_id)

    # store the fairness of the match using the shared TrueSkill
    match.trueskill_quality = env.quality(matchup)

    # shared TrueSkill (participant)
    for team, roster in zip(env.rate(matchup_shared, ranks=[int(not r.winner) for r in match.rosters]),
            match.rosters):
        # lower rank is better = winner!
        for rating, participant in zip(team, roster.participants):
            player = participant.player[0]
            # delta = current - pre
            if player.trueskill_mu is not None:
                participant.trueskill_delta = (float(rating.mu) - float(rating.sigma)) - (float(player.trueskill_mu) - float(player.trueskill_sigma))
            else:
                participant.trueskill_delta = 0
            player.trueskill_mu = rating.mu
            participant.trueskill_mu = rating.mu
            player.trueskill_sigma = rating.sigma
            participant.trueskill_sigma = rating.sigma

    # queue specific TrueSkill (participant_items)
    # no delta
    for team, roster in zip(env.rate(matchup, ranks=[int(not r.winner) for r in match.rosters]),
            match.rosters):
        for rating, participant in zip(team, roster.participants):
            player = participant.player[0]
            pi = participant.participant_items[0]
            setattr(player, trueskill_column + "_mu", rating.mu)
            setattr(pi, trueskill_column + "_mu", rating.mu)
            setattr(player, trueskill_column + "_sigma", rating.sigma)
            setattr(pi, trueskill_column + "_sigma", rating.sigma)


# TODO share this between the two classes
# log errs to stderr, debug to stdout
class InfoFilter(logging.Filter):
    def filter(self, record):
        return record.levelno in (logging.DEBUG, logging.INFO)

logger = logging.getLogger("__name__")
logger.setLevel(logging.INFO)

h1 = logging.StreamHandler(sys.stdout)
h1.setLevel(logging.INFO)
h1.addFilter(InfoFilter())
logger.addHandler(h1)

h2 = logging.StreamHandler(sys.stderr)
h2.setLevel(logging.WARNING)
logger.addHandler(h2)