Tournaments

Introduction

In this tutorial we're going to build a simple tournament system that will allow you to set up tournaments from your back-office using dynamic forms, set tournament details like prizes (Virtual Goods, Bundles, or Currency), set a time for the tournament to go live, and update the tournament based on player activity.

This is going to be a simple elimination tournament where players are allocated into matches each round randomly and, as each player is eliminated, a new round will start with the next group of players until the tournament has a winner.

Important! Mongo Runtime Collections are now deprecated and you'll only be able to follow this tutorial if you are working on a game that you created before January 2018, when the Game Data Service was launched. If you're working on a new game that was created after the Game Data Service was launched in January 2018, you won't be able to create new Mongo Runtime collections in the Collections panel and you'll get an error if you try to do this or if you try to create a Runtime collection using Cloud Code. For details on how to work with the new Game Data Service, see Data Type Explorer.

Tournament Manage Screens

We're going to start this tutorial by creating the management screen tools we need to create new tournaments. If you would like to skip ahead to the Cloud Code examples where we get the tournaments up and running, go to the section on Activating Tournaments.

Note: This tutorial assumes some knowledge of dynamic forms. If you are unfamiliar with dynamic forms, you can see some details on them here, as well as tutorials here. If you would only like to see the cloud-code behind this system, skip to Activating Tournaments section.

To begin with, we're going to create a new screen called Tournaments with the Short Code of tournaments_screen. We'll put some html in this screen that will allow us to draw the snippet that will list all our tournaments later:


1.    <gs-placeholder id="tournaments_ph">   
2.         <gs-snippet snippet="tournaments_view"></gs-snippet>  
3.    </gs-placeholder>

Next, we'll create the tournaments_view snippet.

This snippet is going to be used to draw the list of all tournaments configurations. Here is where we'll see what the tournament structure is going to look like. Our tournaments are going to have the following fields:

  1. status – Indicates if the tournament is inactive, open, live, or finished.
  2. name – The name of the tournament.
  3. description – This will be some flavor-text used to give player details of the tournament when they search for tournaments they can enter.
  4. totalPlayers – This is calculated when you create a new tournament, and will show how many players are required for the tournament to start. In this example, tournaments will start as soon as this number is reached by new players joining.
  5. playersPerMatch – This can be set by the user if you want more than two players per match.
  6. noOfMatches – This is the total number of matches when the match starts. It has to be divisible by 4 in order to get an even number of rounds for each match. If we put in an odd number here, the tournament could still run, but we would have to add extra code to accommodate mismatched players between rounds.
  7. liveDate – This is the date when you want the tournament to go live. It's used by live-ops if you would like the tournament to be configured in advance, and only have the tournament go live later.
  8. rewards – This will include a list of Virtual Goods and Currencies the winner of the tournament will get.

Most of these fields will be good as they are, but there are a few we'll modify before drawing the table. Specifically, we're going to get our list of Virtual Goods and convert it into a comma-separated string, so we can display it in the table. The next value we’ll change is the date, which will be stored as a time-stamp. So, we'll convert this to something human-readable that shows year, month, and day.

There's another step in this snippet, where we'll check the status of the tournament. We need to do this so we can disable the options to edit or delete live tournaments and prevent operators from accidentally editing or deleting tournaments which have live players participating.

So, the following code will go into the JS section of your snippet:


1.    Spark.setScriptData("form", SnippetProcessor());  
2.      
3.    function SnippetProcessor() {  
4.        var form = {};  
5.        return view();  
6.      
7.        function view() { // we need a list of tournaments first. We'll sort these by ID, as the timestamp is also included in the ID, this will filter by newest first //  
8.            var tournamentList = Spark.metaCollection("tournaments").find({}).sort({  
9.                "_id": -1  
10.            }); // we need to edit some details on each tournament, so we'll create an array we can add the edited list to // // this list will be sent in the form //  
11.            var tourList = []; // now we iterate through the cursor //  
12.            while (tournamentList.hasNext()) {  
13.                var tour = tournamentList.next();  
14.                var vgs = ""; // first we are going to create a concatenated string out of our Virtual Goods list, so we can display it in one line in the table  
15.                for (var key in tour.rewards.virtualGoods) {  
16.                    if (tour.rewards.virtualGoods.hasOwnProperty(key)) {  
17.                        vgs += tour.rewards.virtualGoods[key].shortCode + ", ";  
18.                    }  
19.                }  
20.                tour.vgs = vgs.slice(0, -2); // we will convert the mongoDB-date on the tournament into something human-readable  
21.                tour.liveDate = new Date(tour.liveDate).toISOString().substring(0, 16); // we need to remove the option to delete and edit on-going tournaments to protect against errors for those already in tournaments // // we'll use a bool for this to disable the buttons in html //  
22.                if (tour.status === "LIVE") {  
23.                    tour.notEditable = true;  
24.                }  
25.                tourList.push(tour)  
26.            }  
27.            form.tournamentList = tourList;  
28.            return form;  
29.        }  
30.    }

Next, we'll put some HTML in so we can draw the table. Bear in mind that there won’t be any data in this table yet - we'll be creating these tournaments in the next section:

<gs-row>
<gs-title-block title="Tournaments" padding="10" margin="0">
    <gs-row>
        <gs-col width="10"><h4>Here You Can Add, Edit and Tournaments</h4></gs-col>
        <gs-col width="2">
            <gs-link snippet="tournaments_edit?action=view" target="modal-wide"><button>New Tournament</button></gs-link>
        </gs-col>
    </gs-row>
    <hr/>
    <gs-row>
        <gs-row>
        </gs-row>
        <table  border="1" style="width:100%" cellpadding="10">
            <tr align='center' bgcolor="#336699">
                <th><h5>Name</h5></th>
                <th><h5>Status</h5></th>
                <th><h5>Number of Matches</h5></th>
                <th><h5>Players Per Match</h5></th>
                <th><h5>Total Players</h5></th>
                <th><h5>Live Date</h5></th>
                <th><h5>Virtual Goods</h5></th>
                <th><h5>Currency</h5></th>
                <th><h5>Edit</h5></th>
                <th><h5>Delete</h5></th>
            </tr>
            {{#each form.tournamentList}}
            <tr align='center'>
                <td><h5>{{name}}</h5></td>
                {{#if isActive}}
                <td><h5 style="color:green;">LIVE</h5></td>
                {{else}}
                <td><h5>{{status}}</h5></td>
                {{/if}}
                <td><h5>{{noOfMatches}}</h5></td>
                <td><h5>{{playersPerMatch}}</h5></td>
                <td><h5>{{totalPlayers}}</h5></td>
                <td><h5>{{liveDate}}</h5></td>
                <td><h5>{{vgs}}</h5></td>
                <td><h5>{{rewards.currency.currencyType}} [{{rewards.currency.amount}}]</h5></td>
                <td><h5>
                    {{#if notEditable}} Cannot Edit
                    {{else}}
                        <gs-link snippet="tournaments_edit?action=view&tournament_id={{_id.$oid}}" target="modal-wide"><i data-toggle="tooltip" data-placement="top" title="Edit Tournament" class="icon-edit"/></gs-link>
                    {{/if}}
                </h5></td>
                <td><h5>
                    {{#if notEditable}} Cannot Delete
                    {{else}}
                    <gs-link snippet="tournaments_delete?action=view&tournament_id={{_id.$oid}}" target="modal-small"><i data-toggle="tooltip" data-placement="top" title="Delete Tournament" class="icon-trash"/></gs-link>
                    {{/if}}
                </h5></td>
            </tr>
            {{/each}}
        </table>
    </gs-row>
</gs-title-block>
</gs-row>

Below is an example of what this would look like with a valid tournament:

Creating and Editing Tournaments

We'll create a new snippet with the Short Code tournaments_edit. This snippet is where we'll be able to edit or create new tournaments. It will have the same fields we described in the section above.

Let’s start with the HTML needed to draw these fields. These fields will be mostly text and number fields but there will be some complicated fields like the list of rewards and the date-picker that we'll need to configure ourselves using jQuery:

{{#if form.updated}}
    <gs-modal-close></gs-modal-close>
    <gs-snippet snippet="tournaments_view"></gs-snippet>
{{else}}
    <gs-row>
        <gs-title-block title={{#if form.tournament}}"Edit Tournament"{{else}}"Create New Tournament"{{/if}} padding="10" margin="0">
            <gs-form snippet="tournaments_edit?action=save&tournament_id={{form.tournament._id.$oid}}" target="tournaments_ph">
                <gs-row>
                    <gs-col width="3"><h5>Tournament Name</h5></gs-col>
                    <gs-col width="9"><h5>Description</h5></gs-col>
                </gs-row>
                <gs-row>
                    <gs-col width="3">
                        <input type='text' placeholder="Tournament Name..." name='name' value="{{form.tournament.name}}" required/>
                    </gs-col>
                    <gs-col width="9">
                        <input type='text' placeholder="Tournament Description..." name='description' value="{{form.tournament.description}}" required/>
                    </gs-col>
                </gs-row>
                <gs-row>
                    <gs-col width="4">Number Of Matches</gs-col>
                    <gs-col width="4">Players Per Match</gs-col>
                    <gs-col width="4">Go Live Date</gs-col>
                </gs-row>
                <gs-row>
                    <gs-col width="4"><input type='number' step="4" min="4" placeholder="4" name='noOfMatches' value="{{form.tournament.noOfMatches}}" required/></gs-col>
                    <gs-col width="4"><input type='number' min="2" placeholder="2" name='playersPerMatch' value="{{form.tournament.playersPerMatch}}" required/></gs-col>
                    <gs-col width="4"><input type="text" class="datetimepicker" name="liveDate" value="{{form.tournament.liveDate}}" required /></gs-col>
                </gs-row>
                <gs-row>
                    <gs-col width="6">Currency Type</gs-col>
                    <gs-col width="6">Amount</gs-col>
                </gs-row>
                <gs-row>
                    <gs-col width="6">
                        <select name="currencyType" class="input-block-level" style="margin-bottom:5px">
                            {{#each form.gameCurrencies}}
                                <option value="{{shortCode}}" {{#compare shortCode "===" ../form.tournament.rewards.currency.currencyType}}selected{{/compare}}>{{name}}</option>
                            {{/each}}
                        </select>
                    </gs-col>
                    <gs-col width="6"><input type='number' min="1" placeholder="100" name='currencyAmount' value="{{form.tournament.rewards.currency.amount}}"/></gs-col>
                </gs-row>
                <gs-row>
                   <gs-col width="7"><h4>Rewards</h4></gs-col>
                    <gs-col width="2"><h4></h4></gs-col>
                    <gs-col width="1"><h4>Add</h4></gs-col>
                    <gs-col width="2"><a id="add_vg"><i data-toggle="tooltip" data-placement="top" title="Add New Virtual Good" class="icon-plus-sign"></i></a></gs-col>
                </gs-row>

                <gs-placeholder id="vg_list">
                    {{#each form.tournament.rewards.virtualGoods}}
                    <div class='gs-subform' name='vgs' style="border:1px solid black; border-radius: 5px; background: #1e5e42;padding-bottom: 10px; padding-top: 10px;padding-right: 10px;padding-left: 10px;">
                        <gs-row>
                            <gs-col width='10'>
                                <select name="shortCode" class="input-block-level" style="margin-bottom:5px">
                                    {{#each ../form.gameVGs}}
                                        <option value="{{shortCode}}" {{#compare shortCode "===" ../shortCode}}selected{{/compare}}>{{name}}</option>
                                    {{/each}}
                                </select>
                                </gs-col>
                                <gs-col width='2'>
                                <a class="remove_vg"><i data-toggle='tooltip' data-placement='top' title='Remove Item' class='icon-trash'></i></a>
                            </gs-col>
                        </gs-row>
                    </div>
                    {{/each}}
                </gs-placeholder></br>
            <gs-row><gs-col><gs-submit>{{#if form.tournament}}Edit Tournament{{else}}Create New{{/if}}</gs-submit></gs-col></gs-row>
        </gs-title-block><!-- Title Block Ends -->
    </gs-row> <!-- modal row ends -->
</gs-form>
{{/if}}

<div id="gs-subform-vg-template" class='gs-subform' name='vgs' hidden style="border:1px solid black; border-radius: 5px; background: #1e5e42;padding-bottom: 10px; padding-top: 10px;padding-right: 10px;padding-left: 10px;">
    <gs-row>
        <gs-col width='10'>
            <select name="shortCode" class="input-block-level" style="margin-bottom:5px">
                {{#each form.gameVGs}}
                    <option value="{{shortCode}}" {{#compare shortCode "===" ../shortCode}}selected{{/compare}}>{{name}}</option>
                {{/each}}
            </select>
            </gs-col>
            <gs-col width='2'>
            <a class="remove_vg"><i data-toggle='tooltip' data-placement='top' title='Remove Virtual Good' class='icon-trash'></i></a>
        </gs-col>
    </gs-row>
</div>

<script>
    setTimeout(function(){
        // This function will add in a placeholder form with the VG drop-down selector
        $('#add_vg').unbind('click').bind('click', function() {
            $('#gs-subform-vg-template')
                .clone()
                .removeAttr("id")
                .appendTo('div[gs-id="vg_list"]')
                .show();
                bindDelete();
        });

        $( ".datetimepicker" ).datetimepicker({
            dateFormat: "yy-mm-dd",
            separator:"T",
            timeFormat: "HH:mm"
        });
        bindDelete();
    }, 500);

    // this function will all us to delete a placholder containing a VG //
   function bindDelete(){
        $('.remove_vg').unbind('click').bind('click', function() {
            $(this)
                .closest('.gs-subform')
                .remove();
        });


    }
</script>

Most of this is straightforward. The only strange part is with the jQuery we're using to create the Virtual Goods list. You can see that at the bottom of the HTML there is a div with the ID “gs-subform-vg-template”. What the jQuery is doing is adding that div into the ‘vg_list’ placeholder each time you click on the Add button. The opposite is happening when you click on the Delete button. It looks for the last doc with the ‘gs-subform’ ID and removes it from the list. This allows us to get an array of data when we submit the form. And when we want to load that array back in, we can just load the current list. New Virtual Goods will be appended onto this list.

Saving and Editing

We're going to create some JS to edit and save tournaments, but first we need to put in some code to get the Virtual Goods and Currencies we have configured in the portal so we can use them in the drop-down menus. We'll also do a check here for any tournament IDs we passed in - in case we want to edit a tournament, we want to pass that tournament to the HTML form so we can draw the values:

1.    Spark.setScriptData("form", SnippetProcessor(Spark.getData().scriptData))  
2.    function SnippetProcessor(data) {  
3.            var form = {};  
4.            switch (data.action) {  
5.                case "view":  
6.                    return view(data);  
7.                case "save":  
8.                    return save(data);  
9.            }  
10.      
11.            function view(data) { // First we need to load in some data to the form so that the html can draw it // // We need to get a list of all virtual currency names so we can put them into a drop-down menu in the html //  
12.                form.gameCurrencies = Spark.getConfig().getCurrencies();  
13.                form.gameCurrencies.push({  
14.                        "shortCode": "none",  
15.                        "name": "None"  
16.                    }) // Next we need to get the rewards/VGs we have configured in the game and send them to the form //  
17.                    var gameVGs = [];  
18.                var allVGs = Spark.getConfig().getVirtualGoods();  
19.                for (var i = 0; i < allVGs.length; i++) {  
20.                    gameVGs.push({  
21.                        "shortCode": allVGs[i].shortCode,  
22.                        "name": allVGs[i].name  
23.                    });  
24.                }  
25.                form.gameVGs = gameVGs; // now we are going to check if a tournament ID was passed into the snippet // // if there was no ID, then we are creating a new one so we dont have to worry // // if there was an id we will load that tournament so we can pass the details onto the HTML //  
26.                if (data.tournament_id != null) {  
27.                    var tournament = Spark.metaCollection('tournaments').findOne({  
28.                        "_id": {  
29.                            "$oid": data.tournament_id  
30.                        }  
31.                    });  
32.                    tournament.liveDate = new Date(tournament.liveDate).toISOString().substring(0, 16); // we'll convert this date to a format the HTML can read  
33.                    form.tournament = tournament;  
34.                }  
35.                return form;  
36.            }  
37.    }

Saving and editing tournaments is straightforward. We just need to check if we have a tournament ID. If there is an ID present, we update the doc. If not, we insert a new one. There is only one check we need to do here for the Currencies. Our game Currencies have an extra field you can choose from the drop-down menu - “None” - which allows you to set the Currency field to not give the player any Currency as a reward. If this is passed to the script, we just want to leave the Currency empty.

We'll also need to add a function to parse the date-object created in the HTML form.

So, add these functions into the SnippetProcessor() function:


1.    function save(data) {  
2.        var tournamentList = Spark.metaCollection('tournaments');  
3.        if (data.tournament_id && data.tournament_id !== "") {  
4.            updateTournament(tournamentList, data);  
5.        } else {  
6.            insertTournament(tournamentList, data);  
7.        }  
8.        form.updated = true;  
9.        return form;  
10.    }  
11.      
12.    function updateTournament(tournamentList, data) {  
13.        var currencies = {};  
14.        if (data.currencyType !== "none") {  
15.            currencies = {  
16.                "currencyType": data.currencyType,  
17.                "amount": parseInt(data.currencyAmount)  
18.            }  
19.        }  
20.        tournamentList.update({  
21.            "_id": {  
22.                "$oid": data.tournament_id  
23.            }  
24.        }, {  
25.            "$set": {  
26.                "name": data.name,  
27.                "description": data.description,  
28.                "totalPlayers": parseInt(data.noOfMatches * data.playersPerMatch),  
29.                "noOfMatches": parseInt(data.noOfMatches),  
30.                "playersPerMatch": parseInt(data.playersPerMatch),  
31.                "liveDate": dateParse(data.liveDate),  
32.                "rewards": {  
33.                    "virtualGoods": data.vgs,  
34.                    "currency": currencies  
35.                }  
36.            }  
37.        });  
38.    }  
39.      
40.    function insertTournament(tournamentList, data) {  
41.        var currencies = {};  
42.        if (data.currencyType !== "none") {  
43.            currencies = {  
44.                "currencyType": data.currencyType,  
45.                "amount": parseInt(data.currencyAmount)  
46.            }  
47.        }  
48.        tournamentList.insert({  
49.            "status": "INACTIVE",  
50.            "name": data.name,  
51.            "description": data.description,  
52.            "totalPlayers": parseInt(data.noOfMatches * data.playersPerMatch),  
53.            "noOfMatches": parseInt(data.noOfMatches),  
54.            "playersPerMatch": parseInt(data.playersPerMatch),  
55.            "liveDate": dateParse(data.liveDate),  
56.            "rewards": {  
57.                "virtualGoods": data.vgs,  
58.                "currency": currencies  
59.            }  
60.        });  
61.    }

And this function can go outside in the script:


1.    function dateParse(date) {  
2.        var parsedDate = new Date(0);  
3.        var dateTimeParts = date.split("T");  
4.        var dateParts = dateTimeParts[0].split("-");  
5.        var timeParts = dateTimeParts[1].split(":");  
6.        parsedDate.setFullYear(dateParts[0]);  
7.        parsedDate.setMonth(dateParts[1] - 1);  
8.        parsedDate.setDate(dateParts[2]);  
9.        parsedDate.setHours(timeParts[0]);  
10.        parsedDate.setMinutes(timeParts[1]);  
11.        return parsedDate;  
12.    }

You should now have all you need to save and edit tournaments. Any tournaments you create will show up in the tournament list view we created earlier:

Activating Tournaments

The next part of this tutorial will go over how to activate tournaments so they are searchable by players once they go live.

We are going to do this in the EveryMinute script in Cloud-Code.

Warning! EveryMinute is a script you should be very careful with. It has a 30sec timeout, the same as all cloud-code scripts. Therefore, it's never advisable to put player updates in this request, and you should carefully monitor this script’s performance because if the script times out, it will run again in the next 30sec, and timeout again. This can cause a chain of errors that could destabilize your database. MetaCollections are usually fine to check in this script, because most MetaCollections (like these tournaments) won’t have more than a couple of thousand records, so the queries will be pretty efficient.


1.    // In this section we are going to check for any inactive tournaments that we need to set to live // // Once the tournament is set to live it will be searchable by players //  
2.    { // check for tournaments waiting to go live //  
3.        var inactiveTours = Spark.metaCollection("tournaments").find({  
4.            "liveDate": {  
5.                $lte: new Date()  
6.            },  
7.            "status": "INACTIVE"  
8.        });  
9.        if (inactiveTours.count() > 0) {  
10.            require("TOURNAMENTS");  
11.            while (inactiveTours.hasNext()) {  
12.                openTournament(inactiveTours.next())  
13.            }  
14.        }  
15.    }

You’ll notice that this script references a module called TOURNAMENTS so we’ll create that now.

The openTournament() function will be very simple. It’s just going to create a new runtime document which is a snapshot of the tournament data, with some details about the round and player list. Players will be able to search this and choose to join:


1.    /** * This function will set an inactive tournament to live if its live-date has been passed * @param {Object} tournamentData - The metacollection data for the tournament going live */  
2.    function openTournament(tournamentData) {  
3.        Spark.getLog().debug("Opening Tournament - " + tournamentData._id.$oid); // update the status of this tournament so that it is now open to new entries // // first we'll create a new tournament in the live collection //  
4.        var newTour = {  
5.            "status": "OPEN",  
6.            "tournamentData": tournamentData,  
7.            "currentRound": 1,  
8.            "pendingPlayers": [],
9.             "playerList" : []
10.        };  
11.        Spark.runtimeCollection("live_tournaments").insert(newTour) Spark.metaCollection("tournaments").update({  
12.            "_id": tournamentData._id  
13.        }, {  
14.            $set: {  
15.                "status": "OPEN"  
16.            }  
17.        });  
18.        Spark.getLog().debug("Created Live Tournament - " + newTour);  
}

This script runs every minute, so you might have to wait a bit for the tournament to be updated, but you should know the tournament is good to go if you see the status change in the manage screen. You’ll see it go from ‘INACTIVE’ to ‘OPEN’.

Searching Tournaments

The next thing we need to do is give players the ability to search for tournaments and get tournament details. We’ll combine this into one request which will take a tournamentID field. However, if this field is missing, we'll return all tournaments available to the player.

So, in the script for this Event we're going to put the following code:


1.    var tourID = Spark.getData().tournamentID;  
2.    require("TOURNAMENTS"); // If the tournamentID is null then we want to return all tournaments, otherwise we'll return everything //  
3.    if (tourID === "null") {  
4.        Spark.setScriptData("tournament", getOpenTournaments());  
5.    } else {  
6.        Spark.setScriptData("open_tournaments", getTournamentByID(tourID));  
7.    }  
And next we are going to setup these functions in the TOURNAMENTS module.
1.    /** * This function will return all open tournaments*/  
2.    function getOpenTournaments() {  
3.            var tours = Spark.runtimeCollection("live_tournaments").find({  
4.                "status": "OPEN"  
5.            });  
6.            return tours;  
7.        }  
8.        /** * This function will return the tournament information for the ID given * @param {OID} tournamentID - the ID of the tournament we are searching for*/  
9.    function getTournamentByID(tournamentID) {  
10.        var tour = Spark.runtimeCollection("live_tournaments").findOne({  
11.            "_id": {  
12.                "$oid": tournamentID  
13.            }  
14.        });  
15.        if (!tour) {  
16.            Spark.setScriptError("invalid-tournament-id", "Could not find tournament with ID [" + tournamentID + "]") Spark.exit();  
17.        }  
18.        return tour;  
19.    }

All we are doing here is querying either the tournament ID or all tournaments with the status ‘OPEN’. There is only one unusual thing here, which is an error check we do to stop null data being sent back if we didn’t find a tournament with the ID given.

So, now that we can get the tournament information back, we'll next create a request that allows players to join that tournament.

Joining Tournaments

We're going to create a request that will allow a player to join the tournament. We’ll call this joinTournament() and again it will have a single attribute tournamentID, which will be used to specify which tournament the player wants to join.

And we’ll put a few lines into this request now that will call a function from the TOURNAMENTS module we’ll add later:


1.    var tourID = Spark.getData().tournamentID;  
2.    require("TOURNAMENTS");  
3.    var tourData = joinTournament(Spark.getPlayer().getPlayerId(), tourID);  
4.    Spark.setScriptData("tournamentData", tourData);


Note: You’ll also need to create two scriptMessages that we’ll use to send the player information about this tournament. I’ve called these roundBeginsMessage and tournyOpenMessage. Check out the Messages page for more information about how to create custom script messages.

This request will perform the following checks, as well as adding the player to the tournament:

  1. We check the tournamentID is valid (as we did in the section above).
  2. We check that the tournament is open (that is, players can join it).
  3. We check if the player has already joined, since we don’t want to add duplicate players to the tournament.

Other than those checks, this request has a few other pieces that have to work in order for the tournament to start. After we add the last player to the tournament, we have to set it to live. This will just be an update to the tournament doc, but in order to start the tournament, we also have to begin a new round. This round will be complex part. It's broken down into the following actions:

  1. We create a new array on the tournament doc, which will hold the matches for each round.
  2. We'll loop through the pending player list and find random matches based on the number of players that should be in each match. For odd-numbered matches, there's the possibility that the pendingPlayers list will not have enough players to make a full match. That’s okay though, we'll check if there are at least two players in the list and use those two, otherwise we use the playersPerMatch value.
  3. We'll add each match to the round array, and send a message to each player in the match telling them who they have been matched with.
  4. We'll then update the live tournament, and send a message to all players in the tournament telling them the tournament has gone live.

Therefore, our TOURNAMENTS module will need 3 new functions:

JOINTOURNAMENT()


1.    /** * This function will allow a player to join an open tournament * @param {OID} playerID - the ID of the player that wants to join the tournament * @param {OID} tournamentID - the ID of the tournament we are searching for * @return {Object} tournamentData - the updated tournament details*/  
2.    function joinTournament(playerId, tournamentID) { // [1] - Validate the tournamentID is correct, and if so load it  
3.        var tour = getTournamentByID(tournamentID); // [2] - Check that this tournament is actually open to player's joining //  
4.        if (tour.status !== "OPEN") {  
5.            Spark.setScriptError("invalid-tournament-id", "Tournament [" + tournamentID + "] is not open for players to join status [" + tour.status + "]");  
6.            Spark.exit(); // stop the script so no more functions are performed  
7.        } // [3] - Check to see if the player is already in the playerList  
8.        for (var i = 0; i < tour.playerList.length; i++) {  
9.            if (tour.playerList[i] === playerId) {  
10.                Spark.setScriptError("player-already-joined", "Player is already in this tournament...");  
11.                Spark.exit(); // stop the script so no more functions are performed  
12.            }  
13.        } // [4] - Add the player to the player list and the pending player list //  
14.        tour.playerList.push(playerId);  
15.        tour.pendingPlayers.push({  
16.            "playerId": playerId,  
17.            "playerName": Spark.loadPlayer(playerId).getDisplayName()  
18.        }); // [5] - Check if there are enough players to start the tournament //  
19.        if (tour.pendingPlayers.length === tour.tournamentData.totalPlayers) {  
20.            var tour = closeTournament(tour);  
21.            return tour;  
22.        } else { // if there arent enough people to start the tournament, then all we have to do is update the playerList and pendingPlayerList on the original doc  
23.            Spark.runtimeCollection("live_tournaments").update({  
24.                "_id": tour._id  
25.            }, {  
26.                $set: {  
27.                    "playerList": tour.playerList,  
28.                    "pendingPlayers": tour.pendingPlayers  
29.                }  
30.            });  
31.            return tour; // return the updated tournament  
32.        }  
33.    }

CLOSETOURNAMENT()


1.    /** * This function will close a tournament and setup the first round * @param {OID} tour - the tournament Data we want to update * @return {Object} tour- the updated tournament details*/  
2.    function closeTournament(tour) {  
3.        Spark.getLog().debug("Closing Tournament - " + tour._id.$oid); // [1] - Send a message to everyone in the tournament telling them the tournament has started //  
4.        Spark.message("tournyOpenMessage").setMessageData({  
5.            "tournamentID": tour._id.$oid  
6.        }).setPlayerIds(tour.playerList).send(); // [2] - now we can match players for the first round. // // The changes are made to the doc we pass in so saving what we get back from this function will give us the right information we need to update the live tournament //  
7.        var tour = newRound(tour); // [3] - Save the new doc to the live collection and update the tournament meta collection to show this tournament is live //  
8.        Spark.runtimeCollection('live_tournaments').update({  
9.            "_id": tour._id  
10.        }, tour);  
11.        Spark.metaCollection("tournaments").update({  
12.            "_id": tour.tournamentData._id  
13.        }, {  
14.            $set: {  
15.                "status": "LIVE"  
16.            }  
17.        });  
18.        return tour;  
19.    }

NEWROUND()


1.    /** * This function will create a new round out of the pending players * @param {OID} tour - the tournament Data we want to update * @return {Object} tour- the updated tournament details*/  
2.    function newRound(tour) { // [1] - at the start of every round we need to create a new array field with the name of the round we are starting // // We will match player from the pending player list into this array //  
3.        tour["round_" + tour.currentRound] = []; // [2] - We find a number of players to match based on the match size // // We will keep going until all player have been matched. Therefore, we will randomly group players into matches based on the playersPerMatch size // // Each time we find a random player, we will remove then from the pendingList and add them to the round until there are no players left //  
4.        while (tour.pendingPlayers.length >= 2) // we need at least two players to create a match  
5.        {  
6.            var matchGroupSize = (tour.pendingPlayers.length >= tour.tournamentData.playersPerMatch) ? tour.tournamentData.playersPerMatch : tour.pendingPlayers.length;  
7.            var match = [];  
8.            var playerIds = []; // this is only used to keep an array of player IDs we want to send to each player that was just matched  
9.            for (var i = 0; i < matchGroupSize; i++) {  
10.                var randPlayer = Math.floor(Math.random() * tour.pendingPlayers.length);  
11.                match.push(tour.pendingPlayers[randPlayer]);  
12.                playerIds.push(tour.pendingPlayers[randPlayer].playerId) // now we'll remove this player from the pendingPlayerList //  
13.                    tour.pendingPlayers.splice(randPlayer, 1);  
14.            }  
15.            tour["round_" + tour.currentRound].push(match); // send a message to each player in the match to let them know what other players they are matched with //  
16.            Spark.message("roundBeginsMessage").setMessageData({  
17.                "tournamentID": tour._id.$oid,  
18.                "currRound": tour.currentRound,  
19.                "matchDetails": match  
20.            }).setPlayerIds(playerIds).send();  
21.        } // lastly we need to close this tournament so that new players cannot enter it //  
22.        if (tour.status !== "LIVE") {  
23.            tour.status = "LIVE";  
24.        }  
25.        return tour;  
26.    }

Now you’ll need to go through the process of adding players to your tournament to test these functions. If everything goes correctly, you should see the response with the updated tournament information showing each match for round 1. You should also see the roundBeginsMessage and tournyOpenMessage appear if you are testing through the Test Harness:


1.    {  
2.        "@class": ".LogEventResponse",  
3.        "scriptData": {  
4.            "tournamentData": {  
5.                "_id": {  
6.                    "$oid": "5a1fff2c7aadbb04f7647e72"  
7.                },  
8.                "status": "LIVE",  
9.                "tournamentData": {  
10.                    "_id": {  
11.                        "$oid": "5a1fdc5b7aadbb04f754e947"  
12.                    },  
13.                    "status": "OPEN",  
14.                    "name": "Super Awesome Tourny",  
15.                    "description": "Some cool stuff here",  
16.                    "totalPlayers": 8,  
17.                    "noOfMatches": 4,  
18.                    "playersPerMatch": 2,  
19.                    "liveDate": 1512037440000,  
20.                    "rewards": {  
21.                        "virtualGoods": [{  
22.                            "shortCode": "ammo_pack"  
23.                        }, {  
24.                            "shortCode": "machine_gun"  
25.                        }],  
26.                        "currency": {  
27.                            "currencyType": "gold",  
28.                            "amount": 12  
29.                        }  
30.                    }  
31.                },  
32.                "currentRound": 1,  
33.                "pendingPlayers": [],  
34.                "playerList": ["5a200dbfff5b8304ff7f5ad2", "5a200dbfff5b8304ff7f5ad9", "5a200dbfff5b8304ff7f5ae0", "5a200dbfff5b8304ff7f5ae7", "5a200dbfff5b8304ff7f5aee", "5a200dbfff5b8304ff7f5af5", "5a200dbfff5b8304ff7f5afc", "5a200db4ff5b8304ff7f57cc"],  
35.                "round_1": [  
36.                    [{  
37.                        "playerId": "5a200dbfff5b8304ff7f5ad9",  
38.                        "playerName": "Player 3"  
39.                    }, {  
40.                        "playerId": "5a200dbfff5b8304ff7f5ae7",  
41.                        "playerName": "Player 5"  
42.                    }],  
43.                    [{  
44.                        "playerId": "5a200dbfff5b8304ff7f5af5",  
45.                        "playerName": "Player 7"  
46.                    }, {  
47.                        "playerId": "5a200dbfff5b8304ff7f5aee",  
48.                        "playerName": "Player 6"  
49.                    }],  
50.                    [{  
51.                        "playerId": "5a200dbfff5b8304ff7f5ad2",  
52.                        "playerName": "Player 2"  
53.                    }, {  
54.                        "playerId": "5a200db4ff5b8304ff7f57cc",  
55.                        "playerName": "Player 1"  
56.                    }],  
57.                    [{  
58.                        "playerId": "5a200dbfff5b8304ff7f5ae0",  
59.                        "playerName": "Player 4"  
60.                    }, {  
61.                        "playerId": "5a200dbfff5b8304ff7f5afc",  
62.                        "playerName": "Player 8"  
63.                    }]  
64.                ]  
65.            }  
66.        }  
67.    }

Winning Conditions

To finish up this tutorial, we're going to create a request which will set our players as having won or lost a round. Since this is only an example, we're only going to set the player which sends the request first as the winner. In your game, you'll probably want more strict conditions, but the function in our TOURNAMENT module will be the same.

We’ll call this request matchWon, and it will take an attribute called tournamentID.

And we'll put a small piece of code into this request:


1.    var tourID = Spark.getData().tournamentID;  
2.    require("TOURNAMENTS");  
3.    var tourData = winMatch(Spark.getPlayer().getPlayerId(), tourID);  
4.    Spark.setScriptData("tournamentData", tourData);

WINMATCH()

Our winMatch() function will be a bit more complicated than previous functions. There are a few checks we need to perform to begin with. These checks will be similar to before where we checked the tournamentID is valid, and that it’s status is set to ‘LIVE’. But we will also check that the player is in the tournament so we don’t have this function searching for a player that doesn't exist.

An important check we also have to do is to check that the player has already acted in this round. This means checking the player that has won the round, but also that any players in the match that lost can’t use this function to win if they have already lost. We'll do this by marking players in each match with a matchStatus field, which will show they have either won or lost in the current round. We'll also check to see if the player is even in the current round, since players who have been knocked out of the tournament will still have access to the tournament data, but they should not be allowed to perform actions once they have been knocked out.

When we’ve done those checks, we'll find the current player’s match, mark them as having won the round, and send them the roundWonMessage. The player that wins the match is eligible to continue to the next round, so we send them back to the pendingPlayers list. We'll then go through the remaining players in that match and mark them as having lost, and then send them the roundLostMessage.

Winning Condition

So, at this stage, we can check if the tournament has been won. If there's only one match in the current round, the winner of that match is the overall winner. In this case, we'll:

Completing a Round

If we don’t have a winning condition, we have to check if all the matches are finished for the current round. We do this by checking if all matches have a winner. If the count of matches finished is the same as the number of matches on the current round, we increment the currentRound number and call the newRound() function we created earlier.

This will create a new round from the pendingPlayers list, and send messages out to everyone in the new round. We can then update the document.

The function is now complete, and from here we have all the logic we need to run the tournament from start to finish. The following code goes into the TOURNAMENTS module:


1.    /** * This function will match the current player as having one a match, if they pass all the checks to make sure the action is allowed * This function will also check for a winning condition, and increment the round if all matches are completed * @param {OID} playerID - the ID of the player that wants to join the tournament * @param {OID} tournamentID - the ID of the tournament we are searching for * @return {Object} tournamentData - the updated tournament details*/  
2.    function winMatch(playerId, tournamentID) { // [1] - Validate the tournamentID is correct, and if so load it  
3.        var tour = getTournamentByID(tournamentID); // [2] - Check that this tournament is actually open to player's joining //  
4.        if (tour.status !== "LIVE") {  
5.            Spark.setScriptError("invalid-tournament-id", "Tournament [" + tournamentID + "] is no longer live...");  
6.            Spark.exit(); // stop the script so no more functions are preformed  
7.        } // [3] - Check to see if the player is already in the playerList  
8.        var playerInTournament = false;  
9.        for (var i = 0; i < tour.playerList.length; i++) {  
10.            if (tour.playerList[i] === playerId) {  
11.                playerInTournament = true;  
12.            }  
13.        }  
14.        if (!playerInTournament) {  
15.            Spark.setScriptError("player-not-in-tournament", "Player not found in tournament [" + tournamentID + "]");  
16.            Spark.exit(); // stop the script so no more functions are performed  
17.        } // [4] - We need to check if the player has either won or lost already this round. We can do this by finding them in the current round and checking if their match status has been created// //  From this loop we can also check if the player is in the current round at all //  
18.        var playerInCurrentRound = false;  
19.        for (var match = 0; match < tour["round_" + tour.currentRound].length; match++) {  
20.            for (var players = 0; players < tour["round_" + tour.currentRound][match].length; players++) {  
21.                if (playerId === tour["round_" + tour.currentRound][match][players].playerId && tour["round_" + tour.currentRound][match][players].matchStatus !== undefined) {  
22.                    Spark.setScriptError("match-over", "This match is over for the current round...");  
23.                    Spark.exit(); // stop the script so no more functions are performed  
24.                } else if (playerId === tour["round_" + tour.currentRound][match][players].playerId) {  
25.                    playerInCurrentRound = true;  
26.                }  
27.            }  
28.        }  
29.        if (!playerInCurrentRound) {  
30.            Spark.setScriptError("player-not-in-current-round", "Player not found in current round");  
31.            Spark.exit(); // stop the script so no more functions are performed  
32.        } // [5] - Now that we have check if the player can perform an action this round, we find their match and mark them as the winnner // // We will send them a message indicating they've won, and mark all other players in that match as lost, sending those players messages also  
33.        for (var match = 0; match < tour["round_" + tour.currentRound].length; match++) {  
34.            for (var players = 0; players < tour["round_" + tour.currentRound][match].length; players++) {  
35.                if (playerId === tour["round_" + tour.currentRound][match][players].playerId) {  
36.                    matchesFinishedCount++;  
37.                    tour.pendingPlayers.push(JSON.parse(JSON.stringify(tour["round_" + tour.currentRound][match][players]))); // we perform a deep-copy here just so the pending players list doesnt inherit the 'matchStatus' field  
38.                    tour["round_" + tour.currentRound][match][players].matchStatus = "won";  
39.                    Spark.message("roundWonMessage").setMessageData({  
40.                        "tournamentID": tour._id.$oid  
41.                    }).setPlayerIds(tour["round_" + tour.currentRound][match][players].playerId).send();  
42.                    Spark.getLog().debug("Player [" + playerId + "] Won Match - " + tour._id.$oid); // now go over all players in this match that lost and send a message to them //  
43.                    for (var lostPlayers = 0; lostPlayers < tour["round_" + tour.currentRound][match].length; lostPlayers++) {  
44.                        if (playerId !== tour["round_" + tour.currentRound][match][lostPlayers].playerId) {  
45.                            tour["round_" + tour.currentRound][match][lostPlayers].matchStatus = "lost";  
46.                            Spark.message("roundLostMessage").setMessageData({  
47.                                "tournamentID": tour._id.$oid,  
48.                                "winner": tour["round_" + tour.currentRound][match][players]  
49.                            }).setPlayerIds(tour["round_" + tour.currentRound][match][lostPlayers].playerId).send();  
50.                        }  
51.                    }  
52.                }  
53.            }  
54.        } // [6] -Before we check anything else, we can check to see one of these players won by checking if there was only one match left // // if this is the case, we can just update tournament details, deliver the goods, and return the updated tournament //  
55.        if (tour["round_" + tour.currentRound].length === 1) {  
56.            var winner = tour.pendingPlayers[0]; // add a new 'winner' field to the tournament data //  
57.            tour.winner = winner; // and we'll remove this player from the pending player list just to clean up the tournament data structure //  
58.            tour.pendingPlayers.length = 0; // now we'll deliver the VGs and Currency to the player //  
59.            for (var key in tour.tournamentData.rewards.virtualGoods) {  
60.                Spark.loadPlayer(winner.playerId).addVGood(tour.tournamentData.rewards.virtualGoods[key].shortCode, 1, "Tournament Winner [" + tournamentID + "]");  
61.            }  
62.            if (Object.keys(tour.tournamentData.rewards.currency).length > 0) {  
63.                Spark.loadPlayer(winner.playerId).credit(tour.tournamentData.rewards.currency.currencyType, tour.tournamentData.rewards.currency.amount, "Tournament Winner [" + tournamentID + "]");  
64.            }  
65.            Spark.message("tournyWonMessage").setMessageData({  
66.                "tournamentID": tour._id.$oid,  
67.                "winner": winner,  
68.                "rewards": tour.tournamentData.rewards  
69.            }).setPlayerIds(tour.playerIds).send(); // update the tournament //  
70.            tour.status = "FINISHED";  
71.            Spark.metaCollection("tournaments").update({  
72.                "_id": tour.tournamentData._id  
73.            }, {  
74.                $set: {  
75.                    "status": "FINISHED"  
76.                }  
77.            });  
78.            Spark.runtimeCollection('live_tournaments').update({  
79.                "_id": tour._id  
80.            }, tour);  
81.            return tour;  
82.        } // [7] - Now we check to see if that is the last match in this round // // If everyone in this round has moved, then we increase the round and start a new one //  
83.        var matchesFinishedCount = 0; // this will let us keep track of all matches that are finished. We'll use this to tell if a new round should begin  
84.        for (var match = 0; match < tour["round_" + tour.currentRound].length; match++) {  
85.            for (var players = 0; players < tour["round_" + tour.currentRound][match].length; players++) {  
86.                if (tour["round_" + tour.currentRound][match][players].matchStatus === "won") {  
87.                    matchesFinishedCount++;  
88.                    break;  
89.                }  
90.            }  
91.        }  
92.        if (matchesFinishedCount === tour["round_" + tour.currentRound].length) {  
93.            tour.currentRound++;  
94.            tour = newRound(tour);  
95.        } // [8] - And the last step is to save the current config //  
96.        Spark.runtimeCollection('live_tournaments').update({  
97.            "_id": tour._id  
98.        }, tour);  
99.        return tour;  
100.    }

Testing

Through the Test Harness you should be able to test your tournament now. There are a few things you’ll want to test:

  1. Winning a match should show your player’s matchStatus changed to ‘won’ while all other players in the match will be set to ‘lost’. The player who won should be placed in the pendingPlayers list.
  2. That player should not be able to win again this round, and neither should the players that lost be able to perform an action.
  3. Once all players have taken a turn, the round should be updated with new matches, and the pendingPlayers will be empty at the start of the new round.
  4. Players that lost in the previous rounds should not be allowed to perform an action this round.
  5. When the final round is finished, the winner should have some Virtual Goods and Currency delivered to them.
  6. Throughout all these actions, you should see messages being sent to players.

The completed tournament structure should look something like this:


1.    {  
2.        "@class": ".LogEventResponse",  
3.        "scriptData": {  
4.            "tournamentData": {  
5.                "_id": {  
6.                    "$oid": "5a1fff2c7aadbb04f7647e72"  
7.                },  
8.                "status": "FINISHED",  
9.                "tournamentData": {  
10.                    "_id": {  
11.                        "$oid": "5a1fdc5b7aadbb04f754e947"  
12.                    },  
13.                    "status": "OPEN",  
14.                    "name": "Super Awesome Tourny",  
15.                    "description": "Some cool stuff here",  
16.                    "totalPlayers": 8,  
17.                    "noOfMatches": 4,  
18.                    "playersPerMatch": 2,  
19.                    "liveDate": 1512037440000,  
20.                    "rewards": {  
21.                        "virtualGoods": [{  
22.                            "shortCode": "ammo_pack"  
23.                        }, {  
24.                            "shortCode": "machine_gun"  
25.                        }],  
26.                        "currency": {  
27.                            "currencyType": "gold",  
28.                            "amount": 12  
29.                        }  
30.                    }  
31.                },  
32.                "currentRound": 3,  
33.                "pendingPlayers": [],  
34.                "playerList": ["5a2110f57aadbb04f7da766e", "5a2110f57aadbb04f7da7675", "5a2110f57aadbb04f7da767c", "5a2110f57aadbb04f7da7683", "5a2110f57aadbb04f7da768a", "5a2110f57aadbb04f7da7691", "5a2110f57aadbb04f7da7698", "5a2110ee7aadbb04f7da7549"],  
35.                "round_1": [  
36.                    [{  
37.                        "playerId": "5a2110f57aadbb04f7da7683",  
38.                        "playerName": "Player 5",  
39.                        "matchStatus": "won"  
40.                    }, {  
41.                        "playerId": "5a2110f57aadbb04f7da767c",  
42.                        "playerName": "Player 4",  
43.                        "matchStatus": "lost"  
44.                    }],  
45.                    [{  
46.                        "playerId": "5a2110f57aadbb04f7da766e",  
47.                        "playerName": "Player 2",  
48.                        "matchStatus": "won"  
49.                    }, {  
50.                        "playerId": "5a2110f57aadbb04f7da7691",  
51.                        "playerName": "Player 7",  
52.                        "matchStatus": "lost"  
53.                    }],  
54.                    [{  
55.                        "playerId": "5a2110ee7aadbb04f7da7549",  
56.                        "playerName": "Player 1",  
57.                        "matchStatus": "won"  
58.                    }, {  
59.                        "playerId": "5a2110f57aadbb04f7da768a",  
60.                        "playerName": "Player 6",  
61.                        "matchStatus": "lost"  
62.                    }],  
63.                    [{  
64.                        "playerId": "5a2110f57aadbb04f7da7698",  
65.                        "playerName": "Player 8",  
66.                        "matchStatus": "lost"  
67.                    }, {  
68.                        "playerId": "5a2110f57aadbb04f7da7675",  
69.                        "playerName": "Player 3",  
70.                        "matchStatus": "won"  
71.                    }]  
72.                ],  
73.                "round_2": [  
74.                    [{  
75.                        "playerId": "5a2110ee7aadbb04f7da7549",  
76.                        "playerName": "Player 1",  
77.                        "matchStatus": "won"  
78.                    }, {  
79.                        "playerId": "5a2110f57aadbb04f7da7683",  
80.                        "playerName": "Player 5",  
81.                        "matchStatus": "lost"  
82.                    }],  
83.                    [{  
84.                        "playerId": "5a2110f57aadbb04f7da766e",  
85.                        "playerName": "Player 2",  
86.                        "matchStatus": "lost"  
87.                    }, {  
88.                        "playerId": "5a2110f57aadbb04f7da7675",  
89.                        "playerName": "Player 3",  
90.                        "matchStatus": "won"  
91.                    }]  
92.                ],  
93.                "round_3": [  
94.                    [{  
95.                        "playerId": "5a2110ee7aadbb04f7da7549",  
96.                        "playerName": "Player 1",  
97.                        "matchStatus": "won"  
98.                    }, {  
99.                        "playerId": "5a2110f57aadbb04f7da7675",  
100.                        "playerName": "Player 3",  
101.                        "matchStatus": "lost"  
102.                    }]  
103.                ],  
104.                "winner": {  
105.                    "playerId": "5a2110ee7aadbb04f7da7549",  
106.                    "playerName": "Player 1"  
107.                }  
108.            }  
109.        }  
110.    }  

Summary

In this tutorial, we looked at a very simple example of how to get an elimination tournament system up and running from a live-ops back-office tool.

This example is easily expanded upon to accommodate different kinds of tournaments and with different match sizes and rounds (try putting in an odd number into the match-size). But, this example can also be adapted to your own designs with little effort.

You could add in more attributes to the rewards, you could have achievements trigger the match-win scenario, or you could keep a log of each player’s actions throughout each round if you would like every player to perform an action before the round is completed.

This example is just a template for you to adapt to your own game’s needs, so please play around with it as you see fit, and let us know on the forums if you need any help adapting this example to fit your designs.

Did this page help you? Please enter your feedback below. For questions about using this part of the platform, please contact support here