Score Based Asynchronous Challenges

By default, GameSparks Challenges require both players to accept the Challenge before posting their scores. While this is ideal for some games, it's not the best for spontaneously challenging a friend to a game and saying: "Hey! Bet you can't beat this!"

Asynchronous Challenges allow turn-based multiplayer gameplay between two or more players that can check in and play their turn when it's convenient. This is analogous to play-by-mail games and, more electronically, play-by-email.

In this tutorial, we'll use a simple example to show how to set things up to facilitate this kind of Challenge scenario for players of your game:

Asynchronous Challenge Flow

The flow of an Asynchronous Challenge:

1. Player 1 gets an awesome score, decides to challenge another user to beat it.

2. Player 1 picks who to challenge and sends a CreateChallengeRequest to the user of their choice (Player 2).

3. Player 1 receives a successful CreateChallengeResponse, takes the ChallengeInstanceId and sends a LogEventRequest to save their score in that Challenge Instance's ScriptData.

Note: In your code this should all happen very quickly.

4. As a result of the CreateChallengeRequest submitted by Player 1, Player 2 receives a message that Player 1 has challenged them to beat their score.

5. Player 2 sends a GetChallengeRequest to retrieve the latest details of the Challenge (including Player 1's score in the scriptData).

6. Player 2 sends an AcceptChallengeRequest and, on a successful AcceptChallengeResponse, the game loads the appropriate level.

7. Player 2 gets a score and sends a LogChallengeEventRequest with their score and the Cloud Code script will decide the winner.

8. Both Player 1 and Player 2 receive a message declaring who won and who lost.

Creating Events

In this section we go to Configurator > Events and Add two Events.

1. First, create a SetScoreOnChallenge Event. It should look like this:

When Player 1 issues a CreateChallengeRequest, the CreateChallengeResponse will send back a challengeInstanceId. Note that we've added two Attributes to the Event. We can then submit a LogEventRequest for this SetScoreOnChallenge Event using these two Attributes:

Why is LogChallengeEventRequest not used for this Event? The key difference between a LogEventRequest and a LogChallengeEventRequest is that a LogChallengeEventRequest contains an additional challengeInstanceId request parameter. But in this example we'll be using custom Cloud Code to handle this, which means we can use the LogEventRequest - see below for Adding Cloud Code to Events.

2. Click to Save and Close the new SetScoreOnChallenge Event.

3. Second, create the SetScoreAndCalculateOutcome Event, which should look like this:

Note that:

4. Click to Save and Close the new SetScoreAndCalculateOutcome Event.

Setting Up the Challenge

5. Create the Challenge:

Important! Two things to take note of here: First, Turn / Attempt Consumers which has been set to our SetScoreAndCalculateOutcome Event; Second, Leaderboard which has been set to Scripted Outcome and this means we can run custom logic to determine the victor.

6. Click to Save and Close the Challenge.

Adding Cloud Code to Events

7. Add in your Cloud Code:

First, navigate to Configurator > Cloud Code > Events > SetScoreOnChallenge and add in the following:

    //Load our challenge with the challengeId we passed
    var challenge = Spark.getChallenge(Spark.getData().challengeInstanceId);

    //We get the score attribute passed from our game
    var player1Score = Spark.getData().score;

    //Store the turns in our challenge's scriptData
    challenge.setScriptData("player1Score", player1Score);

Second, navigate to Configurator > Cloud Code > Challenge Events > SetScoreAndCalculateOutcome and add in the following:

    //Load our challenge
    var challenge = Spark.getChallenge(Spark.getData().challengeInstanceId);

    //We get the score attribute and save it to the scriptData
    var player2Score = Spark.getData().score;
    challenge.setScriptData("player2Score", player2Score);

    //Player 1 will be our challenger
    var player1 = Spark.loadPlayer(challenge.getChallengerId());

    var player1Score = challenge.getScriptData("player1Score")

    //Player 2 is the player who triggered this event
    var player2 = Spark.getPlayer();
    //We could also load players from challenge.getChallengedPlayerIds()[] or challenge.getAcceptedPlayerIds()[]


    //Call our play function
    play();

    function play()
    {

        if(player1Score > player2Score){

            //challenge.setScriptData("winner", player1);
            challenge.winChallenge(player1);
        }

        if(player2Score > player1Score){

            //challenge.setScriptData("winner", player2);
            challenge.winChallenge(player2);
        }
    }

Ready to Test! With that done we are ready to test.

Checking the Setup in the Test Harness

8. Head on over to the Test Harness, register two players on separate browser tabs, and follow along:

Player 1 gets an awesome score and picks a friend to challenge:

    {
     "@class": ".CreateChallengeRequest",
     "accessType": "PRIVATE",
     "challengeShortCode": "asynchChallenge",
     "endTime": "2015-02-19T00:00Z",
     "usersToChallenge": "54d0b761e4b021c187558337"
    }

We get response back from the service with a chalengeInstanceID:

    {
     "@class": ".CreateChallengeResponse",
     "challengeInstanceId": "54d0d867e4b021c187559b13",

    }

In our game, on a successful CreateChallengeResponse we can now take the newly-created challengeInstanceId and immediately send a LogEventRequest to post Player 1's score to the Challenge using our SetScoreOnChallenge Event:

    {
     "@class": ".LogEventRequest",
     "eventKey": "SetScoreOnChallenge",
     "challengeInstanceId": "54d0d867e4b021c187559b13",
     "score": "356"
    }

This provokes a simple LogEventResponse from the service:

    {
     "@class": ".LogEventResponse",

    }

Player 1 has now challenged their friend and set their score in the scriptData of the Challenge, they can go back to playing their game.

Player 2 will receive a ChallengeIssuedMessage (or push notification!) alerting them that Player 1 has gotten a crazy high score on your game, it will look something like this:

{
 "@class": ".ChallengeIssuedMessage",
 "messageId": "54d0d867e4b021c187559b14",
 "notification": true,
 "summary": ".ChallengeIssuedMessage",
 "who": "oisin",
 "challenge": {
  "challengeId": "54d0d867e4b021c187559b13",
  "state": "ISSUED",
  "scriptData": {},
  "shortCode": "asynchChallenge",
  "startDate": null,
  "accepted": [
   {
    "name": "oisin",
    "id": "54d0b768e4b021c187558356"
   }
  ],
  "challenged": [
   {
    "name": "shane",
    "id": "54d0b761e4b021c187558337"
   }
  ],
  "challenger": {
   "name": "oisin",
   "id": "54d0b768e4b021c187558356"
  },
  "expiryDate": null,
  "endDate": "2015-02-19T00:00Z",
  "challengeName": "Asynchronous Challenge"
 },
 "playerId": "54d0b761e4b021c187558337"
}

You'll notice the scriptData is empty, that's because the message was sent before we logged our score to the Challenge. To overcome this, we can use a GetChallengeRequest:

{
 "@class": ".GetChallengeRequest",
 "challengeInstanceId": "54d0d867e4b021c187559b13"
}

The GetChallengeResponse will reflect the updated Challenge for Player 1's score:

{
 "@class": ".GetChallengeResponse",
 "challenge": {
  "challengeId": "54d0d867e4b021c187559b13",
  "state": "ISSUED",
  "scriptData": {
   "player1Score": "356"
  },
  "shortCode": "asynchChallenge",
  "startDate": null,
  "accepted": [
   {
    "name": "oisin",
    "id": "54d0b768e4b021c187558356"
   }
  ],
  "challenged": [
   {
    "name": "shane",
    "id": "54d0b761e4b021c187558337"
   }
  ],
  "challenger": {
   "name": "oisin",
   "id": "54d0b768e4b021c187558356"
  },
  "expiryDate": null,
  "endDate": "2015-02-19T00:00Z",
  "challengeName": "Asynchronous Challenge"
 },

}

Now that we have the Challenge details, we can display the relevant details to Player 2. Next, we have Player 2 send an AcceptChallengeRequest and launch the level they will be playing:

{
 "@class": ".AcceptChallengeRequest",
 "challengeInstanceId": "54d0d867e4b021c187559b13"
}

Both players will receive a message that Player 2 has accepted the game:

{
 "@class": ".ChallengeStartedMessage",
 "messageId": "54d0db88e4b021c187559cab",
 "notification": true,
 "summary": ".ChallengeStartedMessage",
 "challenge": {
  "challengeId": "54d0d867e4b021c187559b13",
  "state": "RUNNING",
  "scriptData": {
   "player1Score": "356"
  },
  "shortCode": "asynchChallenge",
  "startDate": "2015-02-03T14:30Z",
  "accepted": [
   {
    "name": "oisin",
    "id": "54d0b768e4b021c187558356"
   },
   {
    "name": "shane",
    "id": "54d0b761e4b021c187558337"
   }
  ],
  "challenged": [
   {
    "name": "shane",
    "id": "54d0b761e4b021c187558337"
   }
  ],
  "challenger": {
   "name": "oisin",
   "id": "54d0b768e4b021c187558356"
  },
  "expiryDate": null,
  "endDate": "2015-02-19T00:00Z",
  "challengeName": "Asynchronous Challenge"
 },
 "playerId": "54d0b761e4b021c187558337"
}

Player 2 gets a pretty good score, but not good enough to beat Player 1, so we now submit a LogChallengeEventRequest using the SetScoreAndCalculateOutcome Event:

{
 "@class": ".LogChallengeEventRequest",
 "eventKey": "SetScoreAndCalculateOutcome",
 "challengeInstanceId": "54d0d867e4b021c187559b13",
 "score": "212"
}

Scripted Outcome! Remember that we configured our asynchChallenge for the winner to be determined not by a Leaderboard score but by a Scripted Outcome.

Both Players now get either a ChallengeWonMessage or a ChallengeLostMessage:

Player 2 receives:

{
 "@class": ".ChallengeLostMessage",
 "messageId": "54d0dc43e4b021c187559ce0",
 "notification": true,
 "summary": ".ChallengeLostMessage",
 "winnerName": "oisin",
 "challenge": {
  "challengeId": "54d0d867e4b021c187559b13",
  "state": "COMPLETE",
  "scriptData": {
   "player1Score": "356",
   "player2Score": "212"
  },
  "shortCode": "asynchChallenge",
  "startDate": "2015-02-03T14:30Z",
  "accepted": [
   {
    "name": "oisin",
    "id": "54d0b768e4b021c187558356"
   },
   {
    "name": "shane",
    "id": "54d0b761e4b021c187558337"


}
  ],
  "challenged": [
   {
    "name": "shane",
    "id": "54d0b761e4b021c187558337"
   }
  ],
  "challenger": {
   "name": "oisin",
   "id": "54d0b768e4b021c187558356"
  },
  "expiryDate": null,
  "endDate": "2015-02-19T00:00Z",
  "challengeName": "Asynchronous Challenge"
 },
 "playerId": "54d0b761e4b021c187558337"
}

Player 1 receives:

{
 "@class": ".ChallengeWonMessage",
 "messageId": "54d0dc43e4b021c187559cdd",
 "notification": true,
 "summary": ".ChallengeWonMessage",
 "currency1Won": 0,
 "currency2Won": 0,
 "currency3Won": 0,
 "currency4Won": 0,
 "currency5Won": 0,
 "currency6Won": 0,
 "challenge": {
  "challengeId": "54d0d867e4b021c187559b13",
  "state": "COMPLETE",
  "scriptData": {
   "player1Score": "356",
   "player2Score": "212"
  },
  "shortCode": "asynchChallenge",
  "startDate": "2015-02-03T14:30Z",
  "accepted": [
   {
    "name": "oisin",
    "id": "54d0b768e4b021c187558356"
   },
   {
    "name": "shane",
    "id": "54d0b761e4b021c187558337"
   }
  ],
  "challenged": [
   {
    "name": "shane",
    "id": "54d0b761e4b021c187558337"
   }
  ],
  "challenger": {
   "name": "oisin",
   "id": "54d0b768e4b021c187558356"
  },
  "expiryDate": null,
  "endDate": "2015-02-19T00:00Z",
  "challengeName": "Asynchronous Challenge"
 },
 "playerId": "54d0b768e4b021c187558356"
}

Did this page help you? Please enter your feedback below.