Tiered Leagues #

League play can be used to provide an even more engaging social and competitive gameplay experience.

A league functions as a set of linked leaderboards where each leaderboard represents a particular “tier” of your league’s hierarchy. At the beginning of the league “season”, all players start in the first (bottom) tier. A desired play period is implemented during which players compete against all others in their specific tier. At the end of each play period, a certain set of players at the top of the leaderboard are promoted to the next tier and a certain set at the bottom of the leaderboard are relegated to a lower tier.

Players receive rewards at the end of each play period. The amount and kind of reward is determined by their tier and ranking within it, helping to drive player competition and engagement throughout the league duration.

Leagues can be customized to suit your particular game and needs. The rewards given are dependent on your game’s economy, and you may decide to have players pay an entry fee to join the league. You can create an unlimited number of player tiers, using any promotion and relegation logic desired. This could be the creation of promotion and relegation zones on each leaderboard based on percentages (or other metrics), or a defined number of players (e.g top and bottom 10) to be promoted and relegated after each play period.

For our example below, we use a two tiered league where the top three players after each weekly play period are promoted, and the bottom three players each week are relegated. The in-game currency rewards double in value between the bottom and top tiers of the league.

Create tier leaderboards #

Here we create two linked leaderboards for our league, one each for the lower and top tier, that will reset every week.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Create the two tiers of leaderboards
bottomTierId := "bottom-tier"
topTierId := "top-tier"
authoritative := true
sortOrder := "desc"
operator := "inc"
resetSchedule := "0 0 * * 1"
metadata := make(map[string]interface{})

err := nk.LeaderboardCreate(ctx, bottomTierId, authoritative, sortOrder, operator, resetSchedule, metadata)
if err != nil {
  logger.WithField("err", err).Error("error creating bottom tier leaderboard")
  return err;
}

err = nk.LeaderboardCreate(ctx, topTierId, authoritative, sortOrder, operator, resetSchedule, metadata)
if err != nil {
  logger.WithField("err", err).Error("error creating top tier leaderboard")
  return err;
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Create the two tiers of leaderboards
let bottomTierId = "bottom-tier";
let topTierId = "top-tier";
let authoritative = true;
let sortOrder = nkruntime.SortOrder.DESCENDING;
let operator = nkruntime.Operator.INCREMENTAL;
let resetSchedule = "0 0 * * 1";
let metadata = {};
nk.leaderboardCreate(bottomTierId, authoritative, sortOrder, operator, resetSchedule, metadata);
nk.leaderboardCreate(topTierId, authoritative, sortOrder, operator, resetSchedule, metadata);
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- Create the two tiers of leaderboards
local bottom_tier_id = "bottom-tier"
local top_tier_id = "top-tier"
local authoritative = true
local sort = "desc"
local operator = "best"
local reset = "0 0 * * 1"
local metadata = {}
nk.leaderboard_create(bottom_tier_id, authoritative, sort, operator, reset, metadata)
nk.leaderboard_create(top_tier_id, authoritative, sort, operator, reset, metadata)

Promotion and relegation #

After each weekly play period ends, we promote the top three players from the lower tier to the top tier. Correspondingly, we also relegate the bottom three players from the top tier to the lower one.

Leaderboard promotion and relegation
Leaderboard promotion and relegation

Keep in mind that at the beginning all players are in the bottom tier, so we must first check that there are sufficient players advanced to the top tier before performing any relegation.

Server
 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
// Register leaderboard reset function to handle promotions and relegations
err := initializer.RegisterLeaderboardReset(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, leaderboard *api.Leaderboard, reset int64) error {
  // We're only interested in our top/bottom tier leaderboards so return if it isn't them
  if leaderboard.Id != topTierId && leaderboard.Id != bottomTierId {
    return nil
  }

  // Get all leaderboard records (assuming the tier has no more than 10,000 players)
  records, _, _, _, err := nk.LeaderboardRecordsList(ctx, leaderboard.Id, []string{}, 10000, "", reset)
  if err != nil {
    logger.WithField("err", err).Error("leaderboard record list error")
    return err;
  }

  // If leaderboard is top tier and has 10 or more players, relegate bottom 3 players
  if leaderboard.Id == topTierId && len(records) >= 10 {
    for _, record := range records[len(records)-3:] {
      // Relegate record owner by copying their record into the bottom tier and deleting their current top tier record
      err := nk.LeaderboardRecordWrite(ctx, bottomTierId, record.OwnerId, record.Username.Value, record.Score, record.Subscore, nil, nil)
      if err != nil {
        logger.WithField("err", err).Error("error writing leaderboard record")
        return err
      }

      err = nk.LeaderboardRecordDelete(ctx, topTierId, record.OwnerId)
      if err != nil {
        logger.WithField("err", err).Error("error deleting leaderboard record")
        return err
      }
    }
  }

  // If leaderboard is bottom tier and has 10 or more players, promote top 3 players
  if leaderboard.Id == bottomTierId && len(records) >= 10 {
    for _, record := range records[0:2] {
      // Promote record owner by copying their record into the top tier and deleting their current bottom tier record
      err = nk.LeaderboardRecordWrite(ctx, topTierId, record.OwnerId, record.Username.Value, record.Score, record.Subscore, nil, nil)
      if err != nil {
        logger.WithField("err", err).Error("error writing leaderboard record")
        return err
      }

      err = nk.LeaderboardRecordDelete(ctx, bottomTierId, record.OwnerId)
      if err != nil {
        logger.WithField("err", err).Error("error deleting leaderboard record")
        return err
      }
    }
  }

  return nil
})

if err != nil {
  logger.WithField("err", err).Error("error registering leaderboard reset")
  return err
}
Server
 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
// Register leaderboard reset function to handle promotions and relegations
let leaderboardReset: nkruntime.LeaderboardResetFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, leaderboard: nkruntime.Leaderboard, reset: number) {
  // We're only interested in our top/bottom tier leaderboards so return if it isn't them
  if (leaderboard.id !== topTierId && leaderboard.id !== bottomTierId) {
    return;
  }

  // Get all leaderboard records (assuming the tier has no more than 10,000 players)
  let result = nk.leaderboardRecordsList(leaderboard.id, null, 10000, null, reset);

  // If leaderboard is top tier and has 10 or more players, relegate bottom 3 players
  if (leaderboard.id === topTierId && result.records.length >= 10) {
    // Relegate record owner by copying their record into the bottom tier and deleting their current top tier record
    result.records.slice(result.records.length-3).forEach(function (r) {
      nk.leaderboardRecordWrite(bottomTierId, r.ownerId, r.username, r.score, r.subscore, null, null);
      nk.leaderboardRecordDelete(topTierId, r.ownerId);
    });
  }

  // If leaderboard is bottom tier and has 10 or more players, promote top 3 players
  if (leaderboard.id === topTierId && result.records.length >= 10) {
    // Promote record owner by copying their record into the top tier and deleting their current bottom tier record
    result.records.slice(0, 3).forEach(function (r) {
      nk.leaderboardRecordWrite(topTierId, r.ownerId, r.username, r.score, r.subscore, null, null);
      nk.leaderboardRecordDelete(bottomTierId, r.ownerId);
    });
  }
};
initializer.registerLeaderboardReset(leaderboardReset);
Server
 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
-- Register leaderboard reset function to handle promotions and relegations
local function leaderboardReset(ctx, leaderboard, reset)
  -- We're only interested in our top/bottom tier leaderboards so return if it isn't them
  if leaderboard.id ~= bottom_tier_id and leaderboard.id ~= top_tier_id then
    return
  end

  -- Get all leaderboard records (assuming the tier has no more than 10,000 players)
  local records, _, _, _ = nk.leaderboard_records_list(leaderboard.id, nil, 10000, "", reset)

  -- If leaderboard is top tier and has 10 or more players, relegate bottom 3 players
  if leaderboard.id == top_tier_id and #records >= 10 then
    -- Relegate record owner by copying their record into the bottom tier and deleting their current top tier record
    for i=3, 1, -1 do
      local record = records[#records-i]
      nk.leaderboard_record_write(bottom_tier_id, record.owner, record.username, record.score, record.subscore, {})
      nk.leaderboard_record_delete(top_tier_id, record.owner)
    end
  end

  -- If leaderboard is bottom tier and has 10 or more players, promote top 3 players
  if leaderboard.id == bottom_tier_id and #records >= 10 then
    -- Promote record owner by copying their record into the top tier and deleting their current top tier record
    for i=1, 3, 1 do
      local record = records[i]
      nk.leaderboard_record_write(top_tier_id, record.owner, record.username, record.score, record.subscore, {})
      nk.leaderboard_record_delete(bottom_tier_id, record.owner)
    end
  end
end
nk.register_leaderboard_reset(leaderboardReset)

Rewards #

Leaderboard rewards
Leaderboard rewards

Also at the conclusion of each weekly play period, we distribute rewards to all players inline with their tier and respective ranking within it.

Server
 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
// Register leaderboard reset function to handle promotions and relegations
err := initializer.RegisterLeaderboardReset(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, leaderboard *api.Leaderboard, reset int64) error {
  // Relegation/Promotion code as above...

  // Distribute rewards based on player's tier
  walletUpdates := []*runtime.WalletUpdate{}

  for _, record := range records {
    reward := int64(100)

    // Increase reward for top tier players
    if leaderboard.Id == topTierId {
      reward = 500
    }

    walletUpdates = append(walletUpdates, &runtime.WalletUpdate{
      UserID: record.OwnerId,
      Changeset: map[string]int64 {
        "coins": reward
      },
      Metadata: map[string]interface{}{}
    })
  }

  err := nk.WalletsUpdate(ctx, walletUpdates, true)
  if err != nil {
    logger.WithField("err", err).Error("error updating wallets")
    return err
  }

  return nil
})

if err != nil {
  logger.WithField("err", err).Error("error registering leaderboard reset")
  return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Register leaderboard reset function to handle promotions and relegations
let leaderboardReset: nkruntime.LeaderboardResetFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, leaderboard: nkruntime.Leaderboard, reset: number) {
  // Relegation/Promotion code as above...

  // Distribute rewards based on player's tier
  let walletUpdates : nkruntime.WalletUpdate[];

  result.records.forEach(function (r) {
    let reward = 100;

    // Increase reward for top tier players
    if (leaderboard.id === topTierId) {
      reward = 500;
    }

    walletUpdates.push({
      userId: r.ownerId,
      changeset: { coins: reward},
      metadata: {}
    })
  });

  nk.walletsUpdate(walletUpdates, true);
};
Server
 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
-- Register leaderboard reset function to handle promotions and relegations
local function leaderboardReset(ctx, leaderboard, reset)
  -- Relegation/Promotion code as above...

  -- Distribute rewards based on player's tier
  local wallet_updates = {}

  for i=1, #records do
    local record = records[i]
    local reward = 100

    -- Increase reward for top tier players
    if leaderboard.id == top_tier_id then
      reward = 500
    end

    wallet_updates = table.insert(wallet_updates, {
      user_id = record.owner,
      changeset = {
        coins = reward
      },
      metadata = {}
    })
  end

  nk.wallets_update(wallet_updates, true)
end

Example file #

Download a complete example file for this league: