IodidDOCUMENTATION

formulas/mmr.mdx

/doc/formulas/mmr

PUBLIC DOCUMENTATION

MMR

MMR formula — base per TierDivisionType + LP, range per tier, inversion and competitive projection.

MMR — Estimated Matchmaking Rating

Sources: src/main/java/com/safjnest/lol/utils/TierDivisionUtils.java:78-117,119,136 · src/main/java/com/safjnest/lol/utils/RankProgressUtils.java:44 · src/main/java/com/safjnest/lol/service/CompetitiveService.java:104-120

Iodid MMR is not Riot's internal MMR (never exposed). It is a deterministic continuous score derived from TierDivisionType + LP, used to order leaderboards, compute distribution and resolve contextual ranking. LP remains the value exposed in Rank.

1. TierDivisionUtils.getMmr — base + LP

src/main/java/com/safjnest/lol/utils/TierDivisionUtils.java:78-117:

java
public static int getMmr(TierDivisionType division, int lp) {
    if (division == null || division == TierDivisionType.UNRANKED) return -1;
    int base = switch (division) {
        case IRON_IV -> 0;
        case IRON_III -> 100;
        case IRON_II -> 200;
        case IRON_I -> 300;
        case BRONZE_IV -> 400;
        case BRONZE_III -> 500;
        case BRONZE_II -> 600;
        case BRONZE_I -> 700;
        case SILVER_IV -> 800;
        case SILVER_III -> 900;
        case SILVER_II -> 1000;
        case SILVER_I -> 1100;
        case GOLD_IV -> 1200;
        case GOLD_III -> 1300;
        case GOLD_II -> 1400;
        case GOLD_I -> 1500;
        case PLATINUM_IV -> 1600;
        case PLATINUM_III -> 1700;
        case PLATINUM_II -> 1800;
        case PLATINUM_I -> 1900;
        case EMERALD_IV -> 2000;
        case EMERALD_III -> 2100;
        case EMERALD_II -> 2200;
        case EMERALD_I -> 2300;
        case DIAMOND_IV -> 2400;
        case DIAMOND_III -> 2500;
        case DIAMOND_II -> 2600;
        case DIAMOND_I -> 2700;
        case MASTER_I -> 10000;
        case GRANDMASTER_I -> 20000;
        case CHALLENGER_I -> 30000;
        default -> -1;
    };
    return base < 0 ? -1 : base + Math.max(lp, 0);
}

Rules:

  • UNRANKED or null → -1 (sentinel, excluded from leaderboards).
  • Each lower division is +100 over the previous; the Diamond I → Master I jump is +7300 to clearly separate Apex tiers.
  • Negative LP is clamped to 0 via Math.max(lp, 0) — prevents MMR < base on negative LP (decay or inconsistent API).

Base table (LP=0)

TierDivisionMMR baseLP 0LP 50LP 100
IRON_IV0050100
IRON_III100100150200
BRONZE_IV400400450500
SILVER_IV800800850900
GOLD_IV1200120012501300
PLATINUM_IV1600160016501700
EMERALD_IV2000200020502100
DIAMOND_I2700270027502800
MASTER_I10000100001005010100
GRANDMASTER_I20000200002005020100
CHALLENGER_I30000300003005030100

Example CHALLENGER 500 LP → 30000 + 500 = 30500.

2. MmrRange and getMmrRange

src/main/java/com/safjnest/lol/utils/TierDivisionUtils.java:18,119-134:

java
public record MmrRange(int minimum, Integer maximum) {}

public static MmrRange getMmrRange(TierType tier) {
    return switch (tier) {
      case IRON -> new MmrRange(0, 400);
      case BRONZE -> new MmrRange(400, 800);
      case SILVER -> new MmrRange(800, 1200);
      case GOLD -> new MmrRange(1200, 1600);
      case PLATINUM -> new MmrRange(1600, 2000);
      case EMERALD -> new MmrRange(2000, 2400);
      case DIAMOND -> new MmrRange(2400, 10000);
      case MASTER -> new MmrRange(10000, 20000);
      case GRANDMASTER -> new MmrRange(20000, 30000);
      case CHALLENGER -> new MmrRange(30000, null);
      case UNRANKED -> new MmrRange(-1, 0);
    };
}

Intervals are closed on the left, open on the right [min, max). CHALLENGER has maximum = null (unbounded above). DIAMOND covers [2400, 10000) — includes all 4 Diamond divisions + LP. UNRANKED is [-1, 0).

Used for:

  • competitive query — competitiveFilter(rank, queue, region) translates TierType → MMR range (src/main/java/com/safjnest/nosql/MongoDB.java:1905).
  • Leaderboard distribution — buckets per TierType (MongoDB.findRankDistribution).

3. getTierFromMmr — inversion

src/main/java/com/safjnest/lol/utils/TierDivisionUtils.java:136-143:

java
public static TierType getTierFromMmr(long mmr) {
    for (TierType tier : TierType.values()) {
      if (tier == TierType.UNRANKED) continue;
      MmrRange range = getMmrRange(tier);
      if (mmr >= range.minimum() && (range.maximum() == null || mmr < range.maximum())) return tier;
    }
    return TierType.UNRANKED;
}

Iterates TierType.values() and returns the first tier where mmr ∈ range. For mmr < 0 returns UNRANKED. Deterministic: getTierFromMmr(getMmr(division, lp)) == division.getTier() for every valid division.

4. RankProgressUtils.calculateGain — SoloQ gain

src/main/java/com/safjnest/lol/utils/RankProgressUtils.java:44-57:

java
public static int calculateGain(GameQueueType queue, RankProgress current, RankProgress previous) {
    if (!GameQueueTypeUtils.isRankedSolo(queue) || !hasCurrentSnapshot(current) || !hasCurrentSnapshot(previous)) return 0;
    if (current.rank == TierDivisionType.UNRANKED) return 0;
    if (previous.rank == TierDivisionType.UNRANKED) return current.lp;
    boolean promotionToMaster = previous.rank == TierDivisionType.DIAMOND_I && current.rank == TierDivisionType.MASTER_I;
    boolean masterPlus = current.rank == TierDivisionType.MASTER_I || current.rank == TierDivisionType.GRANDMASTER_I
        || current.rank == TierDivisionType.CHALLENGER_I;
    if ((promotionToMaster || !masterPlus) && current.rank != previous.rank) {
        int gain = 100 - Math.abs(current.lp - previous.lp);
        return current.rank.ordinal() < previous.rank.ordinal() ? gain : -gain;
    }
    return current.lp - previous.lp;
}

Logic only for RANKED_SOLO_5X5 (GameQueueTypeUtils.isRankedSolo, :48 → TEAM_BUILDER_RANKED_SOLO normalized to RANKED_SOLO_5X5):

CaseFormulaExample
same tier/division (intra-division)currentLp - previousLp60 → 80 = +20
promotion/demotion non-Master`100 -lpDiff
demotion non-Master`-(100 -lpDiff
promotion Diamond I → Master Ias above (promotionToMaster case)D1 90 → Master 20 = `100 -
Master/Grandmaster/Challenger intra-ApexcurrentLp - previousLp (pure LP)Master 100 → Master 80 = -20
placement (previous UNRANKED)current.lpUNRANKED → SILVER_IV 30 = +30
non-soloQ or missing snapshot0—

The 100 - |lpDiff| models the virtual 100 LP refill on division change.

5. CompetitiveService.entry — MMR projection

src/main/java/com/safjnest/lol/service/CompetitiveService.java:104-120:

java
static CompetitiveEntry entry(String puuid, LeagueShard shard, Map<GameQueueType, Rank> ranks,
                              ProfileStatistics statistics, GameQueueType queue, long now) {
    GameQueueType canonicalQueue = GameQueueTypeUtils.canonicalQueue(queue);
    Rank rank = ranks == null ? null : ranks.get(canonicalQueue);
    LaneType primary = primary(statistics, CanonicalQueue.from(canonicalQueue));
    Integer otpChampionId = otpChampion(statistics, CanonicalQueue.from(canonicalQueue));
    if (rank == null || rank.tier() == null) return null;
    long mmr = TierDivisionUtils.getMmr(rank.tier(), rank.lp());
    if (mmr < 0) return null;
    return new CompetitiveEntry(puuid, shard, canonicalQueue, rank.tier(), mmr, primary, otpChampionId, now);
}

Each competitive row (_id = sha256(puuid:queue)[0..16], MongoDB.competitiveId:3218) contains mmr = getMmr(tierDivision, lp). UNRANKED / mmr<0 produces no row. primary and otpChampionId derive from canonical ProfileStatistics for role/OTP indexing; if missing the row remains valid but not filterable by those predicates.

6. Invariants

  • getMmr is total and monotonic on TierDivisionType.ordinal() + LP — guarantees stable mmr DESC, _id DESC sort for leaderboard (MongoDB.findLeaderboardPage:1559).
  • getMmrRange and getTierFromMmr are exact inverses on tiers; Apex has width 10000 to contain unbounded LP.
  • calculateGain does not change MMR — it is display-only rankProgress.gain.
  • competitive.mmr is the sole truth for ordering; Rank.lp is never used for sorting.