How TAC rating is calculated | TAC
The rating estimates the playing strength of a member (and a fixed pair) based on recorded game results. This page provides a general overview first, followed by a detailed explanation for developers.
Easy to understand
What is the TAC rating?
The rating is a number that shows how well someone plays TAC — similar to the Elo rating in chess, but using a more modern method. It's calculated solely from games recorded in the app. No one assigns points manually.
There are two types of rating: an individual rating for each member and a team rating for a fixed pair of players (two members who play together).
When is a rating created
A rating is only calculated if at least one actual member plays on both sides. Guests (players without a member account) do not count. If a single member plays only with or against guests, no comparison is made and thus no rating is generated. For a team rating, two members without a guest must play together on one side.
How does the rating change
After every rated game, the rating is adjusted. Winning against stronger opponents increases it significantly; losing against weaker ones decreases it even more. Winning against much weaker opponents barely changes it, as the result was expected.
Every rating also includes an uncertainty: At the start, the system knows little about you, so your rating can still shift significantly. The more you play, the more accurate and stable the assessment becomes.
Provisional rating
As long as a member has fewer than 10 rated games, the rating is considered provisional — it is not yet representative enough for a fair comparison.
Various leaderboards
The rating is tracked separately for several areas to keep comparisons fair:
- Global — across all clubs
- Club — only games within a club.
- Event — only games from a specific event/tournament.
- Season — only games within a club's season.
Inactivity
If you don't play for a while, the estimate of your skill becomes more uncertain over time. The rating itself (the estimated strength) remains the same — only the confidence decreases until you play again.
Technical description (for developers)
Model and starting values
The rating is based on OpenSkill (Bayesian Weng-Lin method with the Plackett-Luce model). The reference implementation runs in Java using the com.pocketcombats:openskill library; the method itself is language-independent and fully described below.
Each rating is a normal distribution with a mean μ (estimated strength) and standard deviation σ (uncertainty). Starting values for a new subject:
μ₀ = 25.0 σ₀ = 25 / 3 ≈ 8.3333
The displayed, comparable value is the conservative ordinal—the strength minus a safety margin for uncertainty:
ordinal = μ − Z · σ (Z = 3)
For the match parameters, the library default values (Weng-Lin) are used without overrides: β = σ₀ / 2 ≈ 4.167 (variance between strength and daily form) and τ = σ₀ / 100 ≈ 0.083 (additive dynamics term that slightly increases σ before each update to keep the rating fluid).
Rate a game
A game consists of two sides (Team A and Team B). Each side is aggregated from the individual ratings of its players into a team rating (sum of μ or sum of σ², DefaultTeamRatingAggregator). The result determines the ranks:
- Winner's side: rank
1, loser's side: rank2. - Draw: both sides rank
1.
Using these ranks, the Plackett-Luce model updates each participant: the strength μ is shifted toward the actual result, and the uncertainty σ decreases with each additional game. The size of the adjustment depends on the difference between the expected and actual result as well as the current uncertainty.
The win probability of Side A against Side B (for predictions/matchmaking) is derived from the aggregated team values using the cumulative normal distribution Φ:
P(A > B) = Φ( (μ_A − μ_B) / √(σ_A² + σ_B² + β²) )
Two projections: individual and pair
Each game is scored in two independent projections:
- Individual — Subject is the member ID. Requires at least one real member on both sides.
- Pair (Team) — the subject is an unordered pair of members. A full side (2 real members) forms a pair; a side with a guest does not form a pair.
Team assignment from the recorded data: result_player.team is A or B; the winner is in result.winner_team (if empty, the game is considered a draw). Players without a member ID (guests) are removed from the rating calculation. If one side consists only of guests, it will not be rated itself, but serves as a standard opponent (subject with starting values) so that the other complete side can still be rated. Games with exclude_from_rating are skipped entirely.
Partitions and chronological repetition
A partition is a tuple (scope, scopeRefId). There are four scopes: GLOBAL (no ref), CLUB (clubId), EVENT (eventId), SEASON (seasonId). Each partition is calculated independently — a game flows into multiple partitions simultaneously.
Ratings are order-dependent and coupled (the result of a game depends on the ratings preceding it). Therefore, they are not "recalculated" incrementally, but are formed by a chronological repetition of the games within the partition. The snapshot history per game (μ/σ before and after each game) is the source of truth; the materialized tables member_rating / team_rating only hold the current state for fast read access.
- Full rebuild: all games in the partition are recalculated from starting values.
- Partial replay from a point in time: the state is loaded from the last snapshot before the point of change (seeding) and replayed from there — snapshots from that point onwards are deleted beforehand.
Materialized values per subject
After the recalculation, the current entry for each subject is rewritten:
| Field | Meaning |
|---|---|
| mu, sigma | Values after the last match |
| ordinal | mu − 3 · sigma (conservative) |
| ordinalPrevious | Rank before the last match (for trend arrow) |
| peakOrdinal / peakAt | Highest rank reached and date |
| gamesCount | Number of rated games in the partition |
| provisional | true, as long as gamesCount < 10 |
| lastGameAt | Time of last rated game |
Inactivity decay
A daily job (Default: 04:00) increases the uncertainty of inactive ratings. For every rating whose last game was more than 30 days ago:
σ ← min(σ + 0.05, 8.3333) ordinal ← μ − 3 · σ
- Per run, σ is increased by
0.05, capped atσ_max = 8.3333(= starting value σ₀). - Only ratings below the upper limit are affected (a batch update statement).
- EVENT scope is exempt from decay (a tournament is a closed period).
Processing (asynchronous, eventual)
Calculation never runs inline with saving a result, but eventually: a result change marks the affected partitions as "dirty" after the commit (AFTER_COMMIT) (adding them to a recompute queue). A single worker processes the queue serially and replays each affected partition from the time of the change. This keeps coupled ratings consistent without blocking the save process.
A result change always affects: GLOBAL, the game's CLUB, the EVENT (old and new, if moved), and every SEASON whose period includes the game time.