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.
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.
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.
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.
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.
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
varcategoryStart=1;varcategoryEnd=2;varstartTime=1538147711;varendTime=null;// all tournaments from the start time
varlimit=100;// number to list per page
varcursor=null;varresult=awaitclient.listTournaments(session,categoryStart,categoryEnd,startTime,endTime,limit,cursor);
Client
1
2
3
4
5
6
7
varcategoryStart=1;varcategoryEnd=2;varstartTime=1538147711;varendTime=null;// all tournaments from the start timevarlimit=100;// number to list per pagevarcursor=null;varresult=awaitclient.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
letcategoryStart=1letcategoryEnd=2letstartTime:Int?=nilletendTime:Int?=nil// all tournaments from the start timeletlimit=100// number to list per pageletcursor:String?=nilvarresult=tryawaitclient.listTournaments(session:session,categoryStart:categoryStart,categoryEnd:categoryEnd,startTime:startTime,endTime:endTime,limit:limit,cursor:cursor)
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constcategoryStart=1;constcategoryEnd=2;conststartTime=null;constendTime=null;// all tournaments from the start time
constlimit=100;// number to list per page
constcursor=null;finalresult=awaitclient.listTournaments(session:session,categoryStart:categoryStart,categoryEnd:categoryEnd,startTime:startTime,endTime:endTime,limit:limit,cursor:cursor,);
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
autosuccessCallback=[](NTournamentListPtrlist){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_tcategoryStart=1;uint32_tcategoryEnd=2;uint32_tstartTime=1538147711;uint32_tendTime=null;// all tournaments from the start time
int32_tlimit=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
intcategoryStart=1;intcategoryEnd=2;intstartTime=1538147711;intendTime=null;// all tournaments from the start timeintlimit=100;// number to list per pageStringcursor=null;TournamentListtournaments=client.listTournaments(session,categoryStart,categoryEnd,startTime,endTime,limit,cursor).get();
Client
1
2
3
4
5
6
7
8
9
10
11
varcategory_start=1varcategory_end=2varstart_time=1538147711varend_time=null# all tournaments from the start timevarlimit=100# number to list per pagevarcursor=nullvarresult:NakamaAPI.ApiTournamentList=yield(client.list_tournaments_async(session,category_start,category_end,start_time,end_time,limit,cursor),"completed")ifresult.is_exception():print("An error occurred: %s"%result)returnprint("Tournaments: %s"%[result])
Client
1
2
3
4
5
6
7
8
9
10
11
varcategory_start=1varcategory_end=2varstart_time=1538147711varend_time=null# all tournaments from the start timevarlimit=100# number to list per pagevarcursor=nullvarresult:NakamaAPI.ApiTournamentList=awaitclient.list_tournaments_async(session,category_start,category_end,start_time,end_time,limit,cursor)ifresult.is_exception():print("An error occurred: %s"%result)returnprint("Tournaments: %s"%[result])
localcategory_start=1localcategory_end=2localstart_time=1538147711localend_time=nil-- all tournaments from the start timelocallimit=100-- number to list per pagelocalcursor=nillocalresult=client.list_tournaments(category_start,category_end,start_time,end_time,limit,cursor)
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.
Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/tournament/<tournament_id>/join"\
-H 'Authorization: Bearer <session token>'
autosuccessCallback=[](){std::cout<<"Successfully joined tournament"<<std::cout;};stringid="someid";client->joinTournament(session,id,successCallback);
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_cacheconfiguration 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.
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
varid="someid";varownerIds=["some","friends","user ids"];varresult=awaitclient.listTournamentRecords(session,id,owenrIds);result.records.forEach(function(record){console.log("Record username %o and score %o",record.username,record.score);});
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.
varid="someid";varscore=100L;varsubscore=10L;// using Nakama.TinyJson;varmetadata=newDictionary<string,string>(){{"weather_conditions","sunny"},{"track_name","Silverstone"}}.ToJson();varnewRecord=awaitclient.WriteTournamentRecordAsync(session,id,score,subscore,metadata);Console.WriteLine(newRecord);
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 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
localid="4ec4f126-3f9d-11e7-84ef-b7c182b36521"localauthoritative=false-- true by defaultlocalsort="desc"-- one of: "desc", "asc"localoperator="best"-- one of: "best", "set", "incr"localreset="0 12 * * *"-- noon UTC each daylocalmetadata={weather_conditions="rain"}title="Daily Dash"description="Dash past your opponents for high scores and big rewards!"category=1start_time=nk.time()/1000-- starts now in secondsend_time=0-- never end, repeat the tournament each day foreverduration=3600-- in secondsmax_size=10000-- first 10,000 players who joinmax_num_score=3-- each player can have 3 attempts to scorejoin_required=true-- must join to competenk.tournament_create(id,authoritative,sort,operator,duration,reset,metadata,title,description,category,start_time,end_time,max_size,max_num_score,join_required)
// 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:=1startTime:=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)iferr!=nil{logger.Printf("unable to create tournament: %q",err.Error())return"",runtime.NewError("failed to create tournament",3)}
letid='4ec4f126-3f9d-11e7-84ef-b7c182b36521';letauthoritative=false;// true by default
letsortOrder=nkruntime.SortOrder.DESCENDING;letoperator=nkruntime.Operator.BEST;letduration=3600;// In seconds.
letresetSchedule='0 12 * * *';// Noon UTC each day.
letmetadata={weatherConditions:'rain',};lettitle='Daily Dash';letdescription="Dash past your opponents for high scores and big rewards!";letcategory=1;letstartTime=0;// Start now.
letendTime=0;// Never end, repeat the tournament each day forever.
letmaxSize=10000;// First 10,000 players who join.
letmaxNumScore=3;// Each player can have 3 attempts to score.
letjoinRequired=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.
err:=nk.TournamentDelete(ctx,id)iferr!=nil{logger.Printf("unable to delete tournament: %q",err.Error())return"",runtime.NewError("failed to delete tournament",3)}
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.
id:="someid"userID:="someuserid"attempts:=10err:=nk.TournamentAddAttempt(ctx,id,userID,attempts)iferr!=nil{logger.Printf("unable to update user %v record attempts: %q",userID,err.Error())return"",runtime.NewError("failed to add tournament attempts",3)}
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.
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:
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.
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.