How ranking works
Every rank score is computed from your recorded games. This page walks through the same logic used on the leaderboard, step by step.
Scope
- Standings are computed separately for each
(group, game_type_code)pair. - A player who plays dominos and mahjongg has two independent rank scores.
- All-time leaderboards apply Bayesian experience adjustment (see Step 8).
- Period leaderboards (year, month, date range) show raw averages only;
experience weighting is disabled for those windows.
Step 1: Per-game inputs
Each GameParticipantResult row supplies:
| Symbol | Meaning |
|---|---|
player_count |
Number of sides in the game (Game.player_count), not head count. A 2-team dominos game with four people has player_count = 2. |
placement |
0-indexed finish position (0 = 1st). Tied scores share the lower place. |
score |
Recorded score for that side. |
result |
Win, loss, tie, or played (no explicit loss). |
double_weight |
2 when the game counts as a double (Dominos winner scored 60+), else 1. |
half_weight |
2 when the game counts as a half (Progressive Rummy), else 1. |
low_wins |
Game-type flag. When true (Progressive Rummy), lower raw scores are better. |
rank_uses_score_magnitude |
Game-type flag on GameTypeDefinition. When false (pickleball), rank credit ignores recorded scores (see Step 2). |
Game weight (used in several steps):
weight = double_weight / half_weight
Examples: a normal game has weight = 1. A double dominos game has
weight = 2. A half Progressive Rummy game has weight = 0.5.
Step 2: Score magnitude (one result)
score_magnitude converts the raw score into a single non-negative number where
larger is always better, regardless of game type. It is used only for rank
credit. Recorded scores on game rows and in stats views are unchanged.
Win-only rank credit (rank_uses_score_magnitude = false, pickleball):
score_magnitude = 1
Every win earns the same placement credit regardless of margin of victory (11–3
vs 11–9 vs rally to 15). Losses and ties still use placement; last place still
earns zero.
High-wins games with score in rank (dominos, mahjongg, backgammon, etc.):
score_magnitude = score
Low-wins games (Progressive Rummy; always uses score in rank), winner
(placement = 0):
score_magnitude = max(500 - score, 0)
The constant 500 is LOW_WINS_REFERENCE_SCORE in code.
Low-wins games, non-winners:
field_max = max(all scores in that game)
score_magnitude = max(field_max - score, 0)
A close second earns more magnitude than a distant second.
The registry helper game_type_rank_uses_score_magnitude(code) selects between
win-only (false) and the formulas above (true). Code calls
rank_score_magnitude_for_result(); there are no game-type if branches in
the ranking service.
Step 3: Side finish points (one result)
side_finish_points is the rank credit the side earned in one game (before
team expansion).
First, placement ratio (how far above last place):
slots_above_last = player_count - 1
placement_distance = max(player_count - placement - 1, 0)
placement_ratio = placement_distance / slots_above_last
Last place always has placement_distance = 0, so side_finish_points = 0.
Then:
side_finish_points = placement_ratio × player_count × weight × score_magnitude
The exponent on placement_ratio is FINISH_POINTS_EXPONENT (currently 1).
Ties multiply the result by 0.5 (TIE_WIN_POINTS_FACTOR).
Examples (no tie, weight = 1, winner):
| Field | player_count |
placement |
score_magnitude |
side_finish_points |
|---|---|---|---|---|
| Pickleball win | 2 | 0 | 1 (win-only) | 1.0 × 2 × 1 × 1 = 2 |
| Dominos win, score 30 | 2 | 0 | 30 | 1.0 × 2 × 1 × 30 = 60 |
| 5-player win, score 30 | 5 | 0 | 30 | 1.0 × 5 × 1 × 30 = 150 |
| 5-player 2nd, score 25 | 5 | 1 | 25 | 0.75 × 5 × 1 × 25 = 93.75 |
| 2-side loss | 2 | 1 | (any) | 0 (placement_ratio = 0) |
For pickleball, two wins at different recorded scores (11 vs 15) each contribute
2 side finish points in a 2-side game.
Step 4: Bayesian factor (experience)
There are two prior strengths, because a specific team pairing accumulates
its own games far more slowly than an individual who pools games across every
partner. Using one shared k for both would either be too lenient for
individuals or too harsh for teams; two constants let each context mature at
its own realistic rate.
k = LEADERBOARD_BAYESIAN_K (default 20, solo-context)
team_k = LEADERBOARD_TEAM_BAYESIAN_K (default 8, team-context)
For a given weighted games count g:
bayesian_factor(g) = g / (g + k)
team_bayesian_factor(g) = g / (g + team_k)
When g = 0, both are 0.
Examples:
g |
bayesian_factor(g), k=20 |
team_bayesian_factor(g), team_k=8 |
|---|---|---|
| 1 | 1/21 ≈ 0.048 | 1/9 ≈ 0.111 |
| 3 | 3/23 ≈ 0.130 | 3/11 ≈ 0.273 |
| 10 | 10/30 ≈ 0.333 | 10/18 ≈ 0.556 |
| 20 | 20/40 = 0.5 | 20/28 ≈ 0.714 |
| 100 | 100/120 ≈ 0.833 | 100/108 ≈ 0.926 |
Solo-context (k=20, compute_bayesian_factor): a person's own rank
score from solo-sourced points, and the Bayesian applied to an individual's
solo-sourced share in Step 8.
Team-context (team_k=8, compute_team_bayesian_factor): a team entity's
own rank score (Step 8), and how much individual credit a member earns from a
team win (Step 5). Both use the pairing's own weighted games as g, never a
member's total games or a member average.
This replaced an older vesting formula (roughly raw × (games/threshold)²).
The "New" badge on the all-time leaderboard is UI-only (games_played < 10);
it does not change any numbers.
Step 5: Crediting finish points to leaderboard rows
Each side credits one or more leaderboard rows. Wins and games_played always
use full weight (+1 win, +weight games) regardless of finish-point
weighting below.
Solo side
The solo player receives:
solo_finish_points += side_finish_points
Team side
The team entity receives full side credit:
team_entity_solo_finish_points += side_finish_points
Each member receives experience-weighted credit (all-time only):
member_team_finish_points += side_finish_points × team_bayesian_factor(member_games_played)
This uses team_bayesian_factor (team_k=8), not the solo bayesian_factor
(k=20), so a group's most active player is not perpetually treated as a
rookie just because their absolute game count is modest.
member_games_played is that member's total weighted games in scope (solo plus
team), computed before this step.
Period-filtered leaderboards skip the multiplier (full side_finish_points).
Totals per row
Each leaderboard row tracks:
finish_points_solo = sum of solo-sourced credits
finish_points_team = sum of team-sourced credits (individuals only)
finish_points_total = finish_points_solo + finish_points_team
Team entities store their full side credit in finish_points_solo only
(finish_points_team = 0).
Step 6: Games played and display stats (one player)
Summed across all credited results:
games_played = sum(weight from each credited result)
Wins, losses, and win/loss percentages use the same weights. They are display
stats and do not change sort order.
Step 7: Raw rank score (one player)
raw_rank_score = finish_points_total / games_played
When games_played = 0, raw_rank_score = 0.
Step 8: Rank score (one player, displayed)
Period-filtered leaderboard:
rank_score = raw_rank_score
All-time leaderboard, team entity row:
rank_score = raw_rank_score × team_bayesian_factor(team_games_played)
team_games_played is the team's own weighted games as that pair (1 for one
David+Karen game, 3 for three David+Sara games). Member career totals are not
used here. Among teams with the same raw average and win rate, more team games
means a higher Bayesian factor and a higher rank score. team_bayesian_factor
uses team_k=8, gentler than the solo k=20, since a specific pairing rarely
accumulates as many games as an individual who rotates partners.
All-time leaderboard, individual person:
Bayesian applies to solo-sourced points only. Team-sourced points were already
scaled at credit time (Step 5).
solo_raw = finish_points_solo / games_played
team_raw = finish_points_team / games_played
rank_score = solo_raw × bayesian_factor(games_played) + team_raw
When games_played = 0, rank_score = 0.
Step 9: Rank order (sorting)
Players are sorted by rank_score descending. Ties break in order:
- Higher
games_played - Higher
finish_points_total - Display name (case-insensitive, A to Z)
Rank numbers (1, 2, 3, ...) are assigned after sorting.
Worked example: solo dominos win
Alice beats Bob in a 2-player dominos game. Alice scores 30, Bob scores 10.
player_count = 2, weight = 1, high-wins.
Alice (winner):
side_finish_points = 1.0 × 2 × 1 × 30 = 60
finish_points_solo = 60
finish_points_team = 0
finish_points_total = 60
games_played = 1
raw_rank_score = 60 / 1 = 60
rank_score = (60/1) × bayesian_factor(1) + 0 = 60 × (1/21) ≈ 2.86
Bob (loser):
side_finish_points = 0
rank_score = 0
Worked example: team win (veteran + rookie)
Nancy has 20 prior solo dominos games. Zach has none. Nancy+Zach win one team
game (side score 30 vs 10). player_count = 2, weight = 1, so
side_finish_points = 60.
After the team game:
| Row | games_played |
finish_points_solo |
finish_points_team |
finish_points_total |
|---|---|---|---|---|
"Nancy+Zach" (team) |
1 | 60 | 0 | 60 |
| Nancy (individual) | 21 | ~1200 (prior solo) | 60 × 21/41 ≈ 30.7 | ~1230.7 |
| Zach (individual) | 1 | 0 | 60 × 1/21 ≈ 2.9 | ≈ 2.9 |
Zach (rookie) rank score:
solo_raw = 0
team_raw = 2.9 / 1 = 2.9
rank_score = 0 + 2.9 = 2.9
Nancy (veteran) rank score (simplified; prior solo total ≈ 1200):
solo_raw ≈ 1200 / 21 ≈ 57.1
team_raw ≈ 30.7 / 21 ≈ 1.5
rank_score ≈ 57.1 × (21/41) + 1.5 ≈ 30.7
Zach no longer inherits Nancy's full 60-point side credit on the individual
leaderboard. The team row "Nancy+Zach" still carries the full 60 with
team Bayesian factor 1 / (1 + 8) from one team game, not member career
totals.
Worked example: pickleball teams with equal win rate
David+Karen: 1 team win. David+Sara: 3 team wins. Both have raw average 2.0
(win-only magnitude, 2-side pickleball). Team-context shrinkage uses team_k=8.
| Team | Team games | Raw | Team Bayesian factor | Rank score |
|---|---|---|---|---|
| David+Karen | 1 | 2.0 | 1/9 ≈ 0.111 | ≈ 0.22 |
| David+Sara | 3 | 2.0 | 3/11 ≈ 0.273 | ≈ 0.55 |
David+Sara ranks higher: same win rate and raw average, more games as a pair.
With team_k=8 (rather than the solo k=20), David+Sara's 3-0 record now
lands close to a strong individual's rank score instead of being buried near
the bottom of a combined leaderboard, since a specific pairing naturally
accumulates far fewer games than an individual who rotates partners.
Other notes
Double and half games
weight flows into side_finish_points, games_played, and wins/losses. A
double game doubles those contributions. A half game halves them. There is no
separate rank multiplier for half games yet.
Win points
win_points = player_count × weight on wins (half weight on ties) is tracked
for display and insights but not used in rank_score or sort order.
People vs team leaderboards
The people leaderboard shows individuals only. The team leaderboard shows team
entities. Both use the formulas above.