Tournaments #

Tournaments are competitions which span a short period where opponents compete over a prize.

This document outlines the design of tournaments for Nakama server. It covers the rules for how players find tournaments, which ones they can join, when they’re allowed to submit scores, and how rewards are distributed when the tournament ends.

Rules #

Tournaments are all expressed as leaderboards with special configurations. When creating a new tournament you can set rules around its scheduling, authoritative status, and number and type of participants. Tournaments are distinguished from leaderboards by the ability to add a maximum number of score attempts an opponent can submit for each given tournament duration.

Tournaments can restrict the number of opponents allowed (i.e. First come, First serve) and enforce an optional join requirement. For example each opponent must join before they can submit scores, and only the first 10,000 opponents are allowed to join.

A tournament can also be played by opponents who are not users. For example a guild tournament can be implemented where score submissions are made by guild ID.

Scheduling #

Tournaments are created programmatically to start in the future or immediately upon creation. At creation each tournament must have a startTime (starts immediately if not set) and duration.

You can optionally set a resetSchedule and an endTime. These values allow for flexible control over how long a tournament can be played before it is reset for the next duration, and when it definitively ends. For example a tournament could be created that starts at noon each day and be played for one hour. This would be expressed with a CRON expression (0 12 * * \*) and a duration of 3600 seconds.

A tournament created with:

  • Only duration starts immediately and ends after the set duration
  • a duration and resetSchedule starts immediately and closes after the set duration, then resets and starts again on the defined schedule
  • a duration, resetSchedule, and endTime starts immediately and closes after the set duration, then resets and starts again on the defined schedule until the end time is reached

If an endTime is set, that timestamp marks the definitive end of the tournament, regardless of any resetSchedule or duration values.

Authoritative tournaments #

Tournaments can be created as either authoritative (default) or non-authoritative. To create a non-authoritative tournament, you must explicitly set the authoritative flag to false when creating the tournament, it cannot be changed later.

For authoritative tournaments, clients cannot submit scores directly to the tournament. All score submissions must be via the server runtime functions.

For non-authoritative tournaments, clients can submit scores directly to the tournament.

List tournaments #

Find tournaments which have been created on the server. Tournaments can be filtered with categories and via start and end times.

Omitting the start and end time parameters returns the ongoing and future tournaments.

Setting the end time parameter to 0 only includes tournaments with no end time set in the results.

Setting end time to a > 0 Unix timestamp acts as an upper bound and only returns tournaments that end prior to it (excluding tournaments with no end time).

Setting the start time to a > 0 Unix timestamp returns any tournaments that start at a later time than it.

Code snippet for this language Swift has not been found. Please choose another language to show equivalent examples.
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/tournament?category_start=<category_start>&category_end=<category_end>&start_time=<start_time>&end_time=<end_time>&limit=<limit>&cursor=<cursor>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
7
var categoryStart = 1;
var categoryEnd = 2;
var startTime = 1538147711;
var endTime = null; // all tournaments from the start time
var limit = 100; // number to list per page
var cursor = null;
var result = await client.listTournaments(session, categoryStart, categoryEnd, startTime, endTime, limit, cursor);
Client
1
2
3
4
5
6
7
var categoryStart = 1;
var categoryEnd = 2;
var startTime = 1538147711;
var endTime = null; // all tournaments from the start time
var limit = 100; // number to list per page
var cursor = null;
var result = await client.ListTournamentsAsync(session, categoryStart, categoryEnd, startTime, endTime, limit, cursor);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](NTournamentListPtr list)
{
    std::cout << "Tournament count " << list->tournaments.size() << std::endl;

    for (auto& tournament : list->tournaments)
    {
        std::cout << "Tournament ID " << tournament.id << ", title " << tournament.title << std::endl);
    }
};

uint32_t categoryStart = 1;
uint32_t categoryEnd = 2;
uint32_t startTime = 1538147711;
uint32_t endTime = null; // all tournaments from the start time
int32_t limit = 100; // number to list per page

client->listTournaments(session, categoryStart, categoryEnd, startTime, endTime, limit, opt::nullopt, successCallback);
Client
1
2
3
4
5
6
7
int categoryStart = 1;
int categoryEnd = 2;
int startTime = 1538147711;
int endTime = null; // all tournaments from the start time
int limit = 100; // number to list per page
String cursor = null;
TournamentList tournaments = client.listTournaments(session, categoryStart, categoryEnd, startTime, endTime, limit, cursor).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var category_start = 1
var category_end = 2
var start_time = 1538147711
var end_time = null # all tournaments from the start time
var limit = 100 # number to list per page
var cursor = null
var result : NakamaAPI.ApiTournamentList = yield(client.list_tournaments_async(session, category_start, category_end, start_time, end_time, limit, cursor), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Tournaments: %s" % [result])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var category_start = 1
var category_end = 2
var start_time = 1538147711
var end_time = null # all tournaments from the start time
var limit = 100 # number to list per page
var cursor = null
var result : NakamaAPI.ApiTournamentList = await client.list_tournaments_async(session, category_start, category_end, start_time, end_time, limit, cursor)
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Tournaments: %s" % [result])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
GET /v2/tournament
  ?category_start=<category_start>
  &category_end=<category_end>
  &start_time=<start_time>
  &end_time=<end_time>
  &limit=<limit>
  &cursor=<cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
local category_start = 1
local category_end = 2
local start_time = 1538147711
local end_time = nil -- all tournaments from the start time
local limit = 100 -- number to list per page
local cursor = nil
local result = client.list_tournaments(category_start, category_end, start_time, end_time, limit, cursor)

Join tournament #

A tournament may need to be joined before the owner can submit scores. This operation is idempotent and will always succeed for the owner even if they have already joined the tournament.

Code snippet for this language Swift has not been found. Please choose another language to show equivalent examples.
Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/tournament/<tournament_id>/join" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
var id = "someid";
var success = await client.joinTournament(session, id);
Client
1
2
var id = "someid";
var success = await client.JoinTournamentAsync(session, id);
Client
1
2
3
4
5
6
7
auto successCallback = []()
{
    std::cout << "Successfully joined tournament" << std::cout;
};

string id = "someid";
client->joinTournament(session, id, successCallback);
Client
1
2
String id = "someid";
client.joinTournament(session, id).get();
Client
1
2
3
4
5
6
var id = "someid"
var success : NakamaAsyncResult = yield(client.join_tournament_async(session, id), "completed")
if success.is_exception():
    print("An error occurred: %s" % success)
    return
print("Joined tournament")
Client
1
2
3
4
5
6
var id = "someid"
var success : NakamaAsyncResult = await client.join_tournament_async(session, id)
if success.is_exception():
    print("An error occurred: %s" % success)
    return
print("Joined tournament")
Client
1
2
3
4
5
POST /v2/tournament/<tournament_id>/join
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
local id = "someid"
local result = client.join_tournament(id)

List tournament records #

Getting the Rank Count
The record list result from calling any tournament list function includes a RankCount property which provides the total number of ranked records in the specified tournament leaderboard. This is only populated for leaderboards that are part of the rank cache (i.e. are not included in the leaderboard.blacklist_rank_cache configuration property).

Fetch a mixed list of tournament records as well as a batch of records which belong to specific owners. This can be useful to build up a leaderboard view which shows the top 100 players as well as the scores between the current user and their friends.

Code snippet for this language Swift has not been found. Please choose another language to show equivalent examples.
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/tournament/<tournament_id>?owner_ids=<owner_ids>&limit=<limit>&cursor=<cursor>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
var id = "someid";
var ownerIds = ["some", "friends", "user ids"];
var result = await client.listTournamentRecords(session, id, owenrIds);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o", record.username, record.score);
});
Client
1
2
3
4
var id = "someid";
var limit = 100;
var cursor = null;
var result = await client.ListTournamentRecordsAsync(session, id, new []{ session.UserId }, limit, cursor);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auto successCallback = [](NTournamentRecordListPtr list)
{
    for (auto& record : list->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }
};

string id = "someid";
client->listTournamentRecords(session, id, opt::nullopt, opt::nullopt, {}, successCallback);
Client
1
2
String id = "someid";
LeaderboardRecordList records = client.listLeaderboardRecords(session, id, session.getUserId()).get();
Client
1
2
3
4
5
6
7
8
var id = "someid"
var limit = 100
var cursor = null
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_async(session, id, [session.user_id], limit, cursor), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Records: %s" % [result])
Client
1
2
3
4
5
6
7
8
var id = "someid"
var limit = 100
var cursor = null
var result : NakamaAPI.ApiTournamentRecordList = await client.list_tournament_records_async(session, id, [session.user_id], limit, cursor)
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Records: %s" % [result])
Client
1
2
3
4
5
GET /v2/tournament/<tournament_id>?owner_ids=<owner_ids>&limit=<limit>&cursor=<cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
local id = "someid"
local result = client.list_leaderboard_records(id, session.user_id)

List tournament records around owner #

Fetch the list of tournament records around the owner.

Code snippet for this language Swift has not been found. Please choose another language to show equivalent examples.
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/tournament/<tournament_id>/owner/<owner_id>?limit=<limit>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
var id = "someid";
var ownerId = "some user ID";
var limit = 100;
var result = await client.listTournamentRecordsAroundOwner(session, id, ownerId, limit);
Client
1
2
3
4
var id = "someid";
var ownerId = session.UserId;
var limit = 100;
var result = await client.ListTournamentRecordsAroundOwnerAsync(session, id, ownerId, limit);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto successCallback = [](NTournamentRecordListPtr list)
{
    for (auto& record : list->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }
};

string id = "someid";
string ownerId = session->getUserId();
int32_t limit = 100;
client->listTournamentRecordsAroundOwner(session, id, ownerId, limit, successCallback);
Client
1
2
3
4
5
String id = "someid";
String ownerId = session.getUserId();
int expiry = -1;
int limit = 100;
TournamentRecordList records = client.listTournamentRecordsAroundOwner(session, id, ownerId, expiry, limit).get();
Client
1
2
3
4
5
6
7
8
var id = "someid"
var owner_id = "some user ID"
var limit = 100
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_around_owner_async(session, id, owner_id, limit), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Records: %s" % [result])
Client
1
2
3
4
5
6
7
8
var id = "someid"
var owner_id = "some user ID"
var limit = 100
var result : NakamaAPI.ApiTournamentRecordList = await client.list_tournament_records_around_owner_async(session, id, owner_id, limit)
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Records: %s" % [result])
Client
1
2
3
4
5
GET /v2/tournament/<tournament_id>/owner/<owner_id>?limit=<limit>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
local id = "someid"
local owner_id = "some user ID"
local limit = 100
local result = client.list_leaderboard_records_around_owner(id, owner_id, limit)

Write tournament record #

Authoritative Leaderboards
For authoritative leaderboards, clients cannot submit scores directly. All score submissions must be via the server runtime functions.

Submit a score and optional subscore to a tournament leaderboard. If the tournament has been configured with join required this will fail unless the owner has already joined the tournament.

Code snippet for this language Swift has not been found. Please choose another language to show equivalent examples.
Client
1
2
3
curl -X PUT "http://127.0.0.1:7350/v2/tournament/<tournament_id>" \
  -H 'Authorization: Bearer <session token>' \
  -d '{"score": 100, "subscore": 10, "metadata": "{\"weather_conditions\": \"sunny\", \"track_name\" : \"Silverstone\" }"}'
Client
1
2
3
4
5
6
7
8
var id = "someid";
var score = 100;
var subscore = 10;
var metadata = {
  "weather_conditions": "sunny",
  "track_name": "Silverstone"
}
var newrecord = client.writeTournamentRecord(session, id, score, subscore, metadata);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var id = "someid";
var score = 100L;
var subscore = 10L;
// using Nakama.TinyJson;
var metadata = new Dictionary<string, string>()
{
    { "weather_conditions", "sunny" },
    { "track_name", "Silverstone" }
}.ToJson();
var newRecord = await client.WriteTournamentRecordAsync(session, id, score, subscore, metadata);
Console.WriteLine(newRecord);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auto successCallback = [this](const NLeaderboardRecord& record)
{
    std::cout << "written tournament record" << std::endl;
};

string id = "someid";
int64_t score = 100;
int64_t subscore = 10;
string metadata = "{\"weather_conditions\": \"sunny\", \"track_name\" : \"Silverstone\" }";
client->writeTournamentRecord(session, id, score, subscore, metadata, successCallback);
Client
1
2
3
4
5
string id = "someid";
int score = 10;
int subscore = 20;
final String metadata = "{\"tarmac\": \"wet\"}";
LeaderboardRecord record = client.writeTournamentRecord(session, id, score, subscore, metadata).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var id = "someid"
var score = 100
var subscore = 10
var metadata = JSON.print({
    "weather_conditions": "sunny",
    "track_name": "Silverstone"
})
var new_record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_tournament_record_async(session, id, score, subscore, metadata), "completed")
if new_record.is_exception():
    print("An error occurred: %s" % new_record)
    return
print("Record: %s" % [new_record])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var id = "someid"
var score = 100
var subscore = 10
var metadata = JSON.stringify({
    "weather_conditions": "sunny",
    "track_name": "Silverstone"
})
var new_record : NakamaAPI.ApiLeaderboardRecord = await client.write_tournament_record_async(session, id, score, subscore, metadata)
if new_record.is_exception():
    print("An error occurred: %s" % new_record)
    return
print("Record: %s" % [new_record])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
PUT /v2/tournament/<tournament_id>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>

{
  "score": 100,
  "subscore": 10,
  "metadata": "{\"weather_conditions\": \"sunny\", \"track_name\" : \"Silverstone\" }"
}
Client
1
2
3
4
5
6
7
8
local id = "someid"
local score = 100
local subscore = 10
local metadata = {
  weather_conditions = "sunny",
  track_name = "Silverstone"
}
local new_record = client.write_leaderboard_record(id, json.encode(metadata), nil, score, subscore)

Authoritative functions #

The runtime functions can be accessed via the server framework and enable custom logic to be used to apply additional rules to various aspects of a tournament. For example it may be required that an opponent is higher than a specific level before they’re allowed to join the tournament.

Create tournament #

Create a tournament with all it’s configuration options.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
local id = "4ec4f126-3f9d-11e7-84ef-b7c182b36521"
local authoritative = false   -- true by default
local sort = "desc"     -- one of: "desc", "asc"
local operator = "best" -- one of: "best", "set", "incr"
local reset = "0 12 * * *" -- noon UTC each day
local metadata = {
  weather_conditions = "rain"
}
title = "Daily Dash"
description = "Dash past your opponents for high scores and big rewards!"
category = 1
start_time = nk.time() / 1000 -- starts now in seconds
end_time = 0                  -- never end, repeat the tournament each day forever
duration = 3600               -- in seconds
max_size = 10000              -- first 10,000 players who join
max_num_score = 3             -- each player can have 3 attempts to score
join_required = true          -- must join to compete
nk.tournament_create(id, authoritative, sort, operator, duration, reset, metadata, title, description, category, start_time, end_time, max_size, max_num_score, join_required)
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// import "github.com/gofrs/uuid"
id := uuid.Must(uuid.NewV4())
authoritative := false   // true by default
sortOrder := "desc"  // one of: "desc", "asc"
operator := "best"   // one of: "best", "set", "incr"
resetSchedule := "0 12 * * *" // noon UTC each day
metadata := map[string]interface{}{}
title := "Daily Dash"
description := "Dash past your opponents for high scores and big rewards!"
category := 1
startTime := int(time.Now().UTC().Unix()) // start now
endTime := 0                         // never end, repeat the tournament each day forever
duration := 3600                     // in seconds
maxSize := 10000                     // first 10,000 players who join
maxNumScore := 3                     // each player can have 3 attempts to score
joinRequired := true                 // must join to compete
err := nk.TournamentCreate(ctx, id.String(), authoritative, sortOrder, operator, resetSchedule, metadata, title, description, category, startTime, endTime, duration, maxSize, maxNumScore, joinRequired)
if err != nil {
      logger.Printf("unable to create tournament: %q", err.Error())
      return "", runtime.NewError("failed to create tournament", 3)
}
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
let id = '4ec4f126-3f9d-11e7-84ef-b7c182b36521';
let authoritative = false;   // true by default
let sortOrder = nkruntime.SortOrder.DESCENDING;
let operator = nkruntime.Operator.BEST;
let duration = 3600;     // In seconds.
let resetSchedule = '0 12 * * *'; // Noon UTC each day.
let metadata = {
  weatherConditions: 'rain',
};
let title = 'Daily Dash';
let description = "Dash past your opponents for high scores and big rewards!";
let category = 1;
let startTime = 0;       // Start now.
let endTime = 0;         // Never end, repeat the tournament each day forever.

let maxSize = 10000;     // First 10,000 players who join.
let maxNumScore = 3;     // Each player can have 3 attempts to score.
let joinRequired = true; // Must join to compete.

try {
  nk.tournamentCreate(id, authoritative, sortOrder, operator, duration, resetSchedule, metadata, title, description, category, startTime, endTime, maxSize, maxNumScore, joinRequired);
} catch (error) {
  // Handle error
}
If you don’t create a tournament with a reset schedule then you must provide it with an end time.

Delete tournament #

Delete a tournament by it’s ID.

Server
1
2
local id = "4ec4f126-3f9d-11e7-84ef-b7c182b36521"
nk.tournament_delete(id)
Server
1
2
3
4
5
err := nk.TournamentDelete(ctx, id)
if err != nil {
    logger.Printf("unable to delete tournament: %q", err.Error())
    return "", runtime.NewError("failed to delete tournament", 3)
}
Server
1
2
3
4
5
6
let id = '4ec4f126-3f9d-11e7-84ef-b7c182b36521';
try {
  nk.tournamentDelete(id);
} catch (error) {
  // Handle error
}

Add score attempts #

Add additional score attempts to the owner’s tournament record. This overrides the max number of score attempts allowed in the tournament for this specific owner.

Tournaments created without a defined maxNumScore have a default limit of 1,000,000 attempts.
Server
1
2
3
4
local id = "someid"
local owner = "someuserid"
local attempts = 10
nk.tournament_add_attempt(id, owner, attempts)
Server
1
2
3
4
5
6
7
8
id := "someid"
userID := "someuserid"
attempts := 10
err := nk.TournamentAddAttempt(ctx, id, userID, attempts)
if err != nil {
    logger.Printf("unable to update user %v record attempts: %q", userID, err.Error())
    return "", runtime.NewError("failed to add tournament attempts", 3)
}
Server
1
2
3
4
5
6
7
8
let id = '4ec4f126-3f9d-11e7-84ef-b7c182b36521';
let owner = 'leaderboard-record-owner';
let count = -10;
try {
  nk.tournamentAddAttempt(id, owner, count);
} catch (error) {
  // Handle error
}

Reward distribution #

When a tournament’s active period ends a function registered on the server will be called to pass the expired records for use to calculate and distribute rewards to owners.

To register a reward distribution function in Go use the initializer.

Server
1
2
3
4
5
local nk = require("nakama")
local function distribute_rewards(_context, tournament, session_end, expiry)
  -- ...
end
nk.register_tournament_end(distribute_rewards)
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import (
    "context"
    "database/sql"
    "log"

    "github.com/heroiclabs/nakama/api"
    "github.com/heroiclabs/nakama/runtime"
)

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
    if err := initializer.RegisterTournamentEnd(distributeRewards); err != nil {
        return err
    }
}

func distributeRewards(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, tournament *api.Tournament, end int64, reset int64) error {
    // ...
    return nil
}
Server
1
2
3
4
5
6
let distributeTournamentRewards: nkruntime.TournamentEndFunction = function(ctx: Context, logger: Logger, nk: Nakama, tournament: Tournament, end: number, reset: number) {
  // ...
}

// Inside InitModule function
initializer.registerTournamentEnd(tournamentEndFn);

A simple reward distribution function which sends a persistent notification to the top ten players to let them know they’ve won and adds coins to their virtual wallets would look like:

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local nk = require("nakama")
local function distribute_rewards(_context, tournament, session_end, expiry)
  local notifications = {}
  local wallet_updates = {}
  local records, owner_records, nc, pc = nk.leaderboard_records_list(tournament.id, nil, 10, nil, expiry)
  for i = 1, #records do
    notifications[i] = {
      code = 1,
      content = { coins = 100 },
      persistent = true,
      subject = "Winner",
      user_id = records[i].owner_id
    }
    wallet_updates[i] = {
      user_id = records[i].owner_id,
      changeset = { coins = 100 },
      metadata = {}
    }
  end

  nk.wallets_update(wallet_updates, true)
  nk.notifications_send(notifications)
end
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func distributeRewards(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, tournament *api.Tournament, end int64, reset int64) error {
    wallets := []*runtime.WalletUpdate{}
    notifications := []*runtime.NotificationSend{}
    content := map[string]interface{}{}
    changeset := map[string]int64{"coins": 100}
    records, _, _, _, err := nk.LeaderboardRecordsList(ctx, tournament.Id, []string{}, 10, "", reset)
    for _, record := range records {
        wallets = append(wallets, &runtime.WalletUpdate{record.OwnerId, changeset, content})
        notifications = append(notifications, &runtime.NotificationSend{record.OwnerId, "Leaderboard winner", content, 1, "", true})
    }
    _, err = nk.WalletsUpdate(ctx, wallets, false)
    if err != nil {
        logger.Error("failed to update winner wallets: %v", err)
        return err
    }
    err = nk.NotificationsSend(ctx, notifications)
    if err != nil {
        logger.Error("failed to send winner notifications: %v", err)
        return err
    }
    return nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
let distributeTournamentRewards: nkruntime.TournamentEndFunction = function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, tournament: nkruntime.Tournament, end: number, reset: number) {
  let notifications: nkruntime.NotificationRequest[] = [];
  let walletUpdates: nkruntime.WalletUpdate[] = []
  let results = nk.leaderboardRecordsList(tournament.id, [], 10, '', reset);
  results.records?.forEach(function (r) {
    notifications.push({
      code: 1,
      content: { coins: 100 },
      persistent: true,
      subject: "Winner",
      userId: r.ownerId,
    });

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

  nk.walletsUpdate(walletUpdates, true)
  nk.notificationsSend(notifications)
}

Advanced #

Tournaments can be used to implement a league system. The main difference between a league and a tournament is that leagues are usually seasonal and incorporate a ladder or tiered hierarchy that opponents can progress on.

A league can be structured as a collection of tournaments which share the same reset schedule and duration. The reward distribution function can be used to progress opponents between one tournament and the next in between each reset schedule.

See the Tiered Leagues guide for an example.

Tournament metadata #

Each tournament and tournament record can optionally include additional data about the tournament itself, or the score being submitted and the score owner. The extra fields must be JSON encoded and submitted as the metadata.

An example use case for metadata is info about race conditions in a driving game, such as weather, which can give extra UI hints when users list records.

1
2
3
4
5
{
  "surface": "wet",
  "timeOfDay": "night",
  "car": "Porsche 918 Spyder"
}