About Invokation

Invokation provides matchmaking and skill-ratings for video games.

Wizard

SBMM: Skill-Based Matchmaking

Definition

Skill-Based Matchmaking (SBMM) is the process of matching together players of similar skill.

Why do games use SBMM?

In short, SBMM increases engagement and retention. This effect has been observed consistently in experiments by game developers1 2 3 and in academic studies4.

The absence of SBMM leads to repetitive outcomes for players. High-skill players will dominate most of their matches, but low-skill players will consistently struggle. Matchmaking without SBMM leads to a greater variety of skills within each match, but less variety of individual outcomes.

Lifetime Win Rate

For the vast majority (>80%) of players, the increased outcome variance with SBMM lets them experience more frequent highs, but with the trade-off of relatively more frequent lows for highly-skilled players.

Problems with SBMM

Reduced sense of mastery

The main problem with SBMM is that it reduces extrinsic feedback. As players increase in skill, they're matched with opponents of equal skill, which makes it harder to gauge absolute skill and personal improvement. Solving this requires deliberate transparency around MMR.

Lack of variety → Burnout

Tight SBMM can lead to always facing the same types of opponents, and to punishing players for trying off-META5 strategies.

If the MMR system does not adapt appropriately (or if there is no SBMM at all), players will be discouraged from experimentation or from simply enjoying the game. It's necessary to tune the tightness of matchmaking against the responsiveness of the MMR system to mitigate this.

Invokation can help you quantify the trade-offs and balance the two.


  1. Call of Duty: The Role of Skill in Matchmaking

  2. TheGamer: Why Apex Legends Has Skill-Based Matchmaking

  3. GameRant: Respawn Entertainment Dev Reveals Why SBMM Will Be the New Norm for Multiplayer Games

  4. Level Up: Leveraging Skill and Engagement to Maximize Player Game-Play in Online Video Games

  5. Most Effective Tactic Available

Matchmaking-Rating (MMR)

MMR is the skill metric used by a matchmaking system.

Coming soon:

  • Why naive systems like "average KD" are pretty bad.
  • Technical details of common MMR systems:
    • Elo
    • Glicko
    • TrueSkill/OpenSkill
    • TrueSkill 2
  • How and why IVK Skill works so well.

IVK Skill API 1.0

Getting Started

You should have received from us:

  • user_id
  • api_key
  • config_id

For most requests, you will also need to provide a match_id. This can be an arbitrary string for testing.

REST API Overview

All requests use Basic Auth:

username: {{user_id}}
password: {{api_key}}

Base URL:

https://dev.ivk.dev/v1/mmr/{{config_id}}

e.g. to test credentials with cURL (and retrieve the configuration JSON):

curl https://dev.ivk.dev/v1/mmr/{{config_id}} -u "{{user_id}}:{{api_key}}"

Sending Match Results

Send the match results as a list of JSON objects in the body of the POST request. (see the examples)

POST https://dev.ivk.dev/v1/mmr/{{config_id}}/full/player/?match_id={{match_id}}

Request Parameters

Most parameters are optional and will fall back to default values when null or omitted.

required

  • player_id (str)
    • The unique identifier of the player.

header data

  • team_id (str|int|null)

    • The local identifier of the player's team, e.g. "Team 1".
    • Default: unique team_id per player.
    • Default behavior is to treat each player as their own team, so team_id can be omitted for 1v1 or free-for-all modes.
  • party_id (str|int|null)

    • The local identifier of the player's party.
    • Default: unique party_id per player.
    • Default behavior is to treat each player as their own party, so party_id can be omitted for games that don't support parties.

pre-match data

  • mmr (float|null)
    • The MMR of the player at the start of the match (the prior MMR).
    • Default: default_mmr specified in the MMR configuration
  • played_frac (float|null)
    • The fraction of the match the player was present.
    • Default: 1.0
  • games_played (int|null)
    • The previous number of games the player has played (ever, or just this season).
    • Default: 0
    • Not necessary if placement matches are disabled in the MMR configuration, but most MMR systems should use some form of placement.

post-match data

  • player_score (float|null)
    • The player's score in this match.
    • Default: 0
    • It's up to the game to determine this value. The only requirement is that a higher score is better than a lower score.
    • This value only matters if player performance is configured to have non-zero weight.
  • team_score (float|null)
    • The player's team's score.
    • Default: the first non-null team_score for the team, otherwise the sum of the player scores on the team (e.g. total kills)
    • This value is normalized according to the MMR configuration, e.g:
      • binary outcome: { loss => 0, win => 1 }
      • margin-of-victory (considers number of rounds won, or difference in score)

optional overrides

  • outcome (float|null)[0-1]
    • For games with custom score normalization or custom player/team models
    • Default: calculated from player_score and team_score using IVK normalization and player/team performance model (recommended).
  • placement_frac (float|null)[0-1]
    • Manually calculated placement fraction
    • Default: calculated from games_played based on MMR configuration (recommended).

Response Attributes

The POST response includes summary data (see the examples) and a list of JSON objects per player in the player_list field:

  • player_id (str)
    • pass-through identifier of the player
  • mmr (float)
    • prior MMR of the player
  • mmr_team (float)
    • total MMR of the player's team as determined by the party and team models (not necessarily the sum of MMRs)
    • can be shown directly in an after-action report
  • played_frac (float)
    • pass-through value
  • placement_frac (float)
    • the prior fraction of placement matches the player completed (0-1)
  • party_size (int)
    • total number of players in the player's party
  • expected (float)
    • the combined expected outcome for the player (0-1)
    • predicted based on combined party, team, and player performance models
    • can be used indirectly in an after-action report
  • outcome (float)
    • the actual outcome for the player (0-1)
    • based on the combined the normalized player and team scores
    • can be used indirectly in an after-action report
  • player_expected (float)
    • the expected individual outcome regardless of team (0-1)
    • potentially clamped to a maximum value (typically 0.8) to guarantee minimum payoffs
  • player_expected_raw (float)
    • the unclamped player_expected value (0-1)
    • can be used indirectly in an after-action report
  • player_outcome (float)
    • the personal outcome for the player (0-1)
    • regardless of party or team factors
  • player_weight (float)
    • how much personal performance factors into the final MMR update
    • configured as a function of placement and/or mmr range
    • typically higher during placement than regular matches
  • player_contrib (float)
    • MMR change due to player performance
    • can be used in an after-action report (sometimes. carefully.)
    • can be negative even if team wins and/or mmr_delta is net positive
    • behavior depends heavily on the specific MMR configuration
  • team_expected (float)
    • the expected team outcome based on party and team model (0-1)
    • potentially clamped to a maximum value (typically 0.8) to guarantee minimum payoffs
  • team_expected_raw (float)
    • the unclamped team_expected value (0-1)
    • can be used indirectly in an after-action report
  • team_outcome (float)
    • the team outcome (0-1)
    • regardless of individual performance
  • team_weight (float)
    • how much team performance factors into the final MMR update
    • configured as a function of placement and/or mmr range
    • typically lower during placement than regular matches
    • often 1.0 after placement is complete in Ranked modes
  • team_contrib (float)
    • MMR change due to team outcome
    • can be used in an after-action report (sometimes. carefully.)
    • can have different sign than player_contrib
    • behavior depends heavily on the specific MMR configuration
  • mmr_delta (float)
    • net MMR change after this match
    • should be shown on after-action report (in Ranked modes)
  • mmr_final (float)
    • final MMR after this match
    • equivalent to mmr + mmr_delta
    • should be shown on after-action report (in Ranked modes)

Advanced

IVK Skill also has routes for calculating pre-match data, or for calculating in-match results (e.g. when a team or player is eliminated in a battle royale). BR has multiple possibilities for MMR though, including tracking separate encounter MMR (based on head-to-head kills) and survival MMR (based on time).

POST https://dev.ivk.dev/v1/mmr/{{config_id}}/pre/player/?match_id={{match_id}}
POST https://dev.ivk.dev/v1/mmr/{{config_id}}/partial/player/?match_id={{match_id}}

IVK Skill API 1.0 - Examples

We recommend using Bruno as an API client. (not Postman)

The most common route for sending match results:

POST https://dev.ivk.dev/v1/mmr/{{config_id}}/full/player/?match_id={{match_id}}

1v1 - Balanced Match

body - 1v1

[
  // Team 1
  {
    "player_id": "T1P1",
    "team_score": 30,
    "player_score": 30,
    "mmr": 400,
    "games_played": 100
  },
  // Team 2
  {
    "player_id": "T2P1",
    "team_score": 40,
    "player_score": 40,
    "mmr": 400,
    "games_played": 100
  }
]

response - 1v1

{
  "success": true,
  "player_count": 2,
  "team_count": 2,
  "result": [
    {
      "player_id": "T1P1",
      "mmr": 400,
      "mmr_team": 0.4,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.5,
      "outcome": 0.009485174635513356,
      "player_expected_raw": 0.5,
      "player_expected": 0.5,
      "player_outcome": 0,
      "player_weight": 0.8,
      "player_contrib": -19.583213144091705,
      "team_expected_raw": 0.5,
      "team_expected": 0.5,
      "team_outcome": 0.04742587317756678,
      "team_weight": 0.2,
      "team_contrib": -4.43142779453245,
      "mmr_delta": -24.014640938624154,
      "mmr_final": 375.98535906137585
    },
    {
      "player_id": "T2P1",
      "mmr": 400,
      "mmr_team": 0.4,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.5,
      "outcome": 0.9905148253644867,
      "player_expected_raw": 0.5,
      "player_expected": 0.5,
      "player_outcome": 1,
      "player_weight": 0.8,
      "player_contrib": 20.416786855908327,
      "team_expected_raw": 0.5,
      "team_expected": 0.5,
      "team_outcome": 0.9525741268224333,
      "team_weight": 0.2,
      "team_contrib": 4.6200547419162215,
      "mmr_delta": 25.036841597824548,
      "mmr_final": 425.03684159782455
    }
  ],
  "debug": null
}

3v3 - Balanced Match

body - 3v3

[
  //
  // Team 1
  //
  {
    "team_id": "T1",
    "player_id": "T1P1",
    "team_score": 30,
    "player_score": 15,
    "mmr": 500,
    "games_played": 100
  },
  {
    "team_id": "T1",
    "player_id": "T1P2",
    "team_score": 30,
    "player_score": 10,
    "mmr": 400,
    "games_played": 100
  },
  {
    "team_id": "T1",
    "player_id": "T1P3",
    "team_score": 30,
    "player_score": 5,
    "mmr": 400,
    "games_played": 100
  },
  //
  // Team 2
  //
  {
    "team_id": "T2",
    "player_id": "T2P1",
    "team_score": 40,
    "player_score": 20,
    "mmr": 500,
    "games_played": 100
  },
  {
    "team_id": "T2",
    "player_id": "T2P2",
    "team_score": 40,
    "player_score": 15,
    "mmr": 450,
    "games_played": 100
  },
  {
    "team_id": "T2",
    "player_id": "T2P3",
    "team_score": 40,
    "player_score": 5,
    "mmr": 350,
    "games_played": 100
  }
]

response - 3v3

{
  "success": true,
  "player_count": 6,
  "team_count": 2,
  "result": [
    {
      "player_id": "T1P1",
      "mmr": 500,
      "mmr_team": 1.3,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.5717913636893147,
      "outcome": 0.5694851746355133,
      "player_expected_raw": 0.5897392046116434,
      "player_expected": 0.5897392046116434,
      "player_outcome": 0.7,
      "player_weight": 0.8,
      "player_contrib": 1.1530945269004178,
      "team_expected_raw": 0.5,
      "team_expected": 0.5,
      "team_outcome": 0.04742587317756678,
      "team_weight": 0.2,
      "team_contrib": -1.1530945269004178,
      "mmr_delta": -0.11530945269004178,
      "mmr_final": 499.88469054730996
    },
    {
      "player_id": "T1P2",
      "mmr": 400,
      "mmr_team": 1.3,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.46381049397388585,
      "outcome": 0.3294851746355134,
      "player_expected_raw": 0.4547631174673573,
      "player_expected": 0.4547631174673573,
      "player_outcome": 0.4,
      "player_weight": 0.8,
      "player_contrib": -2.1448756035963696,
      "team_expected_raw": 0.5,
      "team_expected": 0.5,
      "team_outcome": 0.04742587317756678,
      "team_weight": 0.2,
      "team_contrib": -4.43142779453243,
      "mmr_delta": -6.5763033981288,
      "mmr_final": 393.4236966018712
    },
    {
      "player_id": "T1P3",
      "mmr": 400,
      "mmr_team": 1.3,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.46381049397388585,
      "outcome": 0.08948517463551336,
      "player_expected_raw": 0.4547631174673573,
      "player_expected": 0.4547631174673573,
      "player_outcome": 0.1,
      "player_weight": 0.8,
      "player_contrib": -13.89480349005136,
      "team_expected_raw": 0.5,
      "team_expected": 0.5,
      "team_outcome": 0.04742587317756678,
      "team_weight": 0.2,
      "team_contrib": -4.431427794532437,
      "mmr_delta": -18.3262312845838,
      "mmr_final": 381.6737687154162
    },
    {
      "player_id": "T2P1",
      "mmr": 500,
      "mmr_team": 1.3,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.5717913636893147,
      "outcome": 0.9905148253644867,
      "player_expected_raw": 0.5897392046116434,
      "player_expected": 0.5897392046116434,
      "player_outcome": 1,
      "player_weight": 0.8,
      "player_contrib": 16.41043181553427,
      "team_expected_raw": 0.5,
      "team_expected": 0.5,
      "team_outcome": 0.9525741268224333,
      "team_weight": 0.2,
      "team_contrib": 4.525741268224334,
      "mmr_delta": 20.936173083758604,
      "mmr_final": 520.9361730837586
    },
    {
      "player_id": "T2P2",
      "mmr": 450,
      "mmr_team": 1.3,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.5181319340019225,
      "outcome": 0.7505148253644867,
      "player_expected_raw": 0.5226649175024031,
      "player_expected": 0.5226649175024031,
      "player_outcome": 0.7,
      "player_weight": 0.8,
      "player_contrib": 7.165081511622735,
      "team_expected_raw": 0.5,
      "team_expected": 0.5,
      "team_outcome": 0.9525741268224333,
      "team_weight": 0.2,
      "team_contrib": 4.571473482662635,
      "mmr_delta": 11.73655499428537,
      "mmr_final": 461.73655499428537
    },
    {
      "player_id": "T2P3",
      "mmr": 350,
      "mmr_team": 1.3,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.4108023896865164,
      "outcome": 0.2705148253644867,
      "player_expected_raw": 0.3885029871081455,
      "player_expected": 0.3885029871081455,
      "player_outcome": 0.1,
      "player_weight": 0.8,
      "player_contrib": -11.159640135958671,
      "team_expected_raw": 0.5,
      "team_expected": 0.5,
      "team_outcome": 0.9525741268224333,
      "team_weight": 0.2,
      "team_contrib": 4.376526947614297,
      "mmr_delta": -6.783113188344373,
      "mmr_final": 343.2168868116556
    }
  ],
  "debug": null
}

3v3 - Leaver

body - leaver

[
  //
  // Team 1
  //
  {
    "team_id": "T1",
    "player_id": "T1P1",
    "team_score": 30,
    "player_score": 15,
    "mmr": 500,
    "games_played": 100
  },
  {
    "team_id": "T1",
    "player_id": "T1P2",
    "team_score": 30,
    "player_score": 10,
    "played_frac": 0.5, // <-- was only in half of the match
    "mmr": 400,
    "games_played": 100
  },
  {
    "team_id": "T1",
    "player_id": "T1P3",
    "team_score": 30,
    "player_score": 5,
    "mmr": 400,
    "games_played": 100
  },
  //
  // Team 2
  //
  {
    "team_id": "T2",
    "player_id": "T2P1",
    "team_score": 40,
    "player_score": 20,
    "mmr": 500,
    "games_played": 100
  },
  {
    "team_id": "T2",
    "player_id": "T2P2",
    "team_score": 40,
    "player_score": 15,
    "mmr": 450,
    "games_played": 100
  },
  {
    "team_id": "T2",
    "player_id": "T2P3",
    "team_score": 40,
    "player_score": 5,
    "mmr": 350,
    "games_played": 100
  }
]

response - leaver

{
  "success": true,
  "player_count": 6,
  "team_count": 2,
  "result": [
    {
      "player_id": "T1P1",
      "mmr": 500,
      "mmr_team": 1.1,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.5293052378801512,
      "outcome": 0.5694851746355133,
      "player_expected_raw": 0.5897392046116434,
      "player_expected": 0.5897392046116434,
      "player_outcome": 0.7,
      "player_weight": 0.8,
      "player_contrib": 4.410431815534266,
      "team_expected_raw": 0.2875693709541823,
      "team_expected": 0.2875693709541823,
      "team_outcome": 0.04742587317756678,
      "team_weight": 0.2,
      "team_contrib": -2.401434977766158,
      "mmr_delta": 2.008996837768109,
      "mmr_final": 502.0089968377681
    },
    {
      "player_id": "T1P2",
      "mmr": 400,
      "mmr_team": 1.1,
      "played_frac": 0.5,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.42132436816472235,
      "outcome": 0.3294851746355134,
      "player_expected_raw": 0.4547631174673573,
      "player_expected": 0.4547631174673573,
      "player_outcome": 0.4,
      "player_weight": 0.8,
      "player_contrib": -2.1448756035963883,
      "team_expected_raw": 0.2875693709541823,
      "team_expected": 0.2875693709541823,
      "team_outcome": 0.04742587317756678,
      "team_weight": 0.2,
      "team_contrib": -2.3513906510635976,
      "mmr_delta": -4.496266254659986,
      "mmr_final": 395.50373374534
    },
    {
      "player_id": "T1P3",
      "mmr": 400,
      "mmr_team": 1.1,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.42132436816472235,
      "outcome": 0.08948517463551336,
      "player_expected_raw": 0.4547631174673573,
      "player_expected": 0.4547631174673573,
      "player_outcome": 0.1,
      "player_weight": 0.8,
      "player_contrib": -13.894803490051348,
      "team_expected_raw": 0.2875693709541823,
      "team_expected": 0.2875693709541823,
      "team_outcome": 0.04742587317756678,
      "team_weight": 0.2,
      "team_contrib": -2.351390651063578,
      "mmr_delta": -16.246194141114927,
      "mmr_final": 383.7538058588851
    },
    {
      "player_id": "T2P1",
      "mmr": 500,
      "mmr_team": 1.3,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.6142774894984783,
      "outcome": 0.9905148253644867,
      "player_expected_raw": 0.5897392046116434,
      "player_expected": 0.5897392046116434,
      "player_outcome": 1,
      "player_weight": 0.8,
      "player_contrib": 16.410431815534242,
      "team_expected_raw": 0.7124306290458177,
      "team_expected": 0.7124306290458177,
      "team_outcome": 0.9525741268224333,
      "team_weight": 0.2,
      "team_contrib": 2.401434977766153,
      "mmr_delta": 18.811866793300396,
      "mmr_final": 518.8118667933004
    },
    {
      "player_id": "T2P2",
      "mmr": 450,
      "mmr_team": 1.3,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.560618059811086,
      "outcome": 0.7505148253644867,
      "player_expected_raw": 0.5226649175024031,
      "player_expected": 0.5226649175024031,
      "player_outcome": 0.7,
      "player_weight": 0.8,
      "player_contrib": 7.165081511622751,
      "team_expected_raw": 0.7124306290458177,
      "team_expected": 0.7124306290458177,
      "team_outcome": 0.9525741268224333,
      "team_weight": 0.2,
      "team_contrib": 2.4257012653981826,
      "mmr_delta": 9.590782777020934,
      "mmr_final": 459.59078277702093
    },
    {
      "player_id": "T2P3",
      "mmr": 350,
      "mmr_team": 1.3,
      "played_frac": 1,
      "placement_frac": 1,
      "party_size": 1,
      "expected": 0.45328851549567994,
      "outcome": 0.2705148253644867,
      "player_expected_raw": 0.3885029871081455,
      "player_expected": 0.3885029871081455,
      "player_outcome": 0.1,
      "player_weight": 0.8,
      "player_contrib": -11.159640135958677,
      "team_expected_raw": 0.7124306290458177,
      "team_expected": 0.7124306290458177,
      "team_outcome": 0.9525741268224333,
      "team_weight": 0.2,
      "team_contrib": 2.322259331731681,
      "mmr_delta": -8.837380804226996,
      "mmr_final": 341.162619195773
    }
  ],
  "debug": null
}