question

brendan avatar image
brendan asked

[Design choices] Invitation system

johntube
started a topic on Wed, 16 September 2015 at 5:28 PM

Hi PlayFab community,

I want to seek your advice for a best approach in a design of an invitation system in CloudScript.

Requirements and constraints:

  • It's for 2 players (pvp) games. Invitations can also be called challenges.

  • The invitation system in question requires "2 ways consent": the challenged/receiver should accept.

  • Although I like the optimistic approach of challenging opponents and entering the game directly to start playing, I decided not to do it in this game as each player can start with some power ups equipped. If the opponent declines the challenge, what should happen to those power ups? "redeemed" or lost for ever?

  • Also pending invitations cloud be canceled by the sender.

  • Pending invitations expire after a while.

First let's start with the things that I'm certain of their necessity:

  • Each user should have a shared group with id = {playfabId}_InvitationsList. Or any other format / suffix / prefix.

  • Invitations are unique and should have unique IDs. I'm using the same generator for the game IDs as the invitation will become a game later and keep the same ID. Of course, even if exchanged, this is invisible to the user.

  • CloudScript file has at least 2 handlers to be able to SendInvitation and UpdateInvitation (the latter can be exploded to AcceptInvitation, DeclineInvitation, CancelInvitation): 2 or more will make your ClourScript file long and ugly enough. It depends on how ugly you want it. Arguments parsing ugly with extra bandwidth consumption for the user or longer code with a lot of repetitive stuff. "DRY...and KISS".

Having all the above in place after hours of pain with CloudScript debugging and JSON to C# parsing I managed to have the following:

  • Player1 sends invitation calls RunCloudScript, ActionId="SendInvitation" with Args
{

"GameId", // InvitationId

"PlayerName", // sender or challenger, to be saved and used in the Push Notification msg!

"OpponentId", // receiver or challenged

"OpponentName", // receiver or challenged, to be saved for future Push Notification use.

}

In the respective handler, invitation data should be saved as a value in the 2 shared groups acting as invitation lists of both players. The key is the invitationId. InvitationStatus enum is added to the invitation data, it represents values like SentPending, ReceivedDeclined, SentExpired, ReceivedCanceled, etc.

5 server API calls in SendInvitation only:

2*GetSharedGroupData: to authorize action (check uniqueness, etc)

2*UpdateSharedGroupData: to add new invitation

1*SendPushNotification

  • Player2 receives invitation via Push or by Polling (Periodic or Pull to refresh). Other ways exist like Photon Chat, Facebook API, etc.

  • Player2 Accepts or Declines by calling the appropriate handler.

5 API calls also:

2*GetSharedGroupData: to authorize operation (check invitation exists and state transition is possible, etc.) + CHECK EXPIRATION

2*UpdateSharedGroupData: to update new state

1*SendPushNotification: to inform opponent

can be reduced to 4 when not checking existence and status of invitation in shared group of the original sender (!=currentPlayFabId).

The same logic can also apply to CancelInvitation.

Now instead of being happy of what I achieved I started questioning everything (believe me this happened to me infinite times especially with CloudScript, the estimated time of completion of my game has doubled due to major breaking refactoring):

Do I need a Master_InvitationsList? If I don't need it right now, maybe I will need it in the future who know? ==> I will save 2 API call to server.UpdateSharedGroup per UpdateInvitation handler(s). Same shared invitation data for both. In this case what to save as value in the players' invitations' list? keep it empty? With Polling in mind, a GetInvitationsList handler (data included) will require 1 extra API call to server.GetSharedGroupData. Please let me explain:

  • first get list of invitations of currentPlayFabId

  • then get their respective data using the previously obtained invitationIDs (shared group keys)

  • CHECK EXPIRATION

  • compare with what the list sent by the user:

  • if an id is missing return the whole data to the user

  • if id exists but state is different return state only

Instead of marking invitations with outgoing or incoming maybe I need to keep 2 lists (2 shared groups) per user: IncomingInvitations and OutgoingInvitations? Will double each previous call to get list of invitations though.

Argh...too many questions, I'm not against trying everything and I love JavaScript but...I need to say this outloud: CloudScript I hate you.

Is this "premature optimization"...the root of all evil?

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

1 Answer

·
brendan avatar image
brendan answered

6 Comments
johntube said on Wed, 16 September 2015 at 5:30 PM

sorry about formatting.

I copy pasted this from a backup as it failed the first time I tried posting due to timeout I think.


Brendan Vanous said on Mon, 21 September 2015 at 7:00 PM

Sorry about the delay on this, Hamza. But yes, premature optimization can be really evil. :)

First, let me call out one question (the easy one):

If the opponent declines the challenge, what should happen to those power ups? "redeemed" or lost for ever?

Personally, I'd give them back. Otherwise, you run the risk of groups deciding to mess with people higher up the leaderboard by always declining their challenges.

Now, for the main body of your design. Why not use the PlayFab ID of the player making the invite, rather than generate a Game ID? Are you allowing for more than one active challenge from the same player? This would also eliminate some of the calls in your flow, since you wouldn't need to check for outstanding requests when issuing a challenge - just overwrite it. That means there'd be a tiny chance that a player could accept a challenge at the exact same moment the other player was updating the challenge, but it's not clear that this would be bad, necessarily.

So, I'm seeing your flow as:

Player 1 issues a challenge

  • Updates Player 2's incoming challenges Shared Group Data, overwriting any existing challenge

  • Updates Player 1's outgoing challenges Shared Group Data, overwriting any existing challenge

Player 2 signs in

  • Get Player 2's incoming challenges Shared Group Data

Player 2 accepts or rejects the challenge

  • Updates Player 2's incoming challenges Shared Group Data

  • Updates Player 1's outgoing challenges Shared Group Data

On the accept part of the story, the question is, do you want a player with an active game in progress with another player to be able to issue another challenge to that player before the game is complete? If so, you will want to do a get on the data when issuing a challenge, to check if it's allowed, and you'll likely want to re-use the incoming challenges space as the space to track on active games.

I think the main thing I'm missing is really the in-game flow. If what I've got here doesn't cover what you have in mind, can you walk us through the flow from the player's perspective, and include any requirements or restrictions?

Brendan


johntube said on Tue, 22 September 2015 at 3:39 PM

Hi Brendan,

Thank you for your response, it means a lot as I know you are busy and sometimes I'm asking too many questions and some of my questions may be a little bit "broad".

The flow you just described is exactly identical to the one I sent you via email on July when I asked you to review the CloudScript implementation of Photon's GetGameList WebRPC (which I'm still waiting for btw). I'm sure you will answer that when you have time.

I changed my original design of the invitation system (which I drafted for some Photon customers) for two reasons:

  • I did not make decision yet with my partner if we will allow multiple challenges between same couple of players or not (other than same player issuing multiple challenges to the same opponent, if player A challenges player B, at the same time can player B challenge player A?)

  • Most important reason: we want to keep history of all game related events/actions intact so we can calculate all kinds of statistics later. We liked the Ruzzle's monetization system which is selling stats to the players. But maybe we need to proactively pre-calculate some stats on the go instead like "# of [total (respectively "accepted", "canceled", "expired")] challenges Player A sent to (respectively "received from") Player B" and all other types of head2head stats win/loss/draw/timeout etc.

We can UpdateUserStatistics directly in the CloudScript handlers.

  • Third reason: pending/open challenges and ongoing games can expire/timeout, we may use an external service like webscript to make cron jobs to query all those entries to check if they are still valid. We need a central master invitations list for that.

  • Your reply came too late, meanwhile I couldn't stop myself from questioning everything and starting from scratch again for the nth time.

I hope you're enjoying Unite '15 at Boston. I hope I could make it to one Unite event one day.

Hamza


Brendan Vanous said on Tue, 22 September 2015 at 4:20 PM

Hi again,

My apologies - your mail got backlogged due to Gamescom, as I said, which resulted in it getting buried. I'm reviewing it now, and I'll send you feedback.

Understood on the issue of multiple outstanding challenges between players, but both game history and "cron job expiration" could be an issue.

For game history, what you are describing is keeping statistics for all played games. The way to do this would be using the LogEvent call to write out custom events for your game, that you could then use however you like.

For expiration of challenges, I would recommend just having the clients check on and expire any challenges - ones they sent or received - that are unused. After all, if neither player in a challenge has logged in, it really doesn't matter if the challenge isn't expired, and it'll expire as soon as one of them logs in. An admin tool that tries to pull down a high volume of data is going to see significant delays, as once you're past 1 MB in response data, the data retrieval is throttled. And a tool that makes calls to the service at a high frequency runs the risk of causing the title to be throttled. I realize these would only be issues for high DAU levels, but that's the goal, right?

And thanks, we have a couple of folks at Unite Boston - they're meeting with folks and getting the word out. For anyone else reading this who is there, feel free to stop by and introduce yourselves!

Brendan


johntube said on Tue, 22 September 2015 at 5:23 PM

Hi again,

I want to be able to tell a Player A via Push Notification that he has X hours left to make a move in a game against Player B [as the latter has made his move already and waiting in order to move to the next round or to end the game] or he will risk losing with forfeit as the round times out. I hope you understand the real need of the CRON job here.

Right now the expiration check is done "on demand":

  • when fetching the invitations/games lists/data [this happens onLogin, during the periodic polling or if upon user request with pull to refresh]

  • when a call to a CloudScript handler that will try to update an invitation/game is made.

I have to read more about LogEvent before answering on that matter.

Hamza


Brendan Vanous said on Wed, 23 September 2015 at 12:43 AM

I'm not saying a CRON job is out of the question (after all, we made the Admin and Server APIs to give you the tools to manage your game), but rather that we need to understand the specifics of how it would work. For example, reading from a Shared Group Data - if it's 1 MB of total data, that should be quite speedy. More than that, and there will be an increasing level of delay in getting the data back, due to throttling. And then how often the read is called, and how many Push Notification calls are made is also important to understand. If it's possible this could result in hundreds of calls being made all at once (which it sounds like is a possibility), we should talk through the specifics, to see if there's a better path.

All in all, what you're really attempting to do is leverage existing functionality to build a new server-side feature which we don't offer (yet). Regardless of the feature, if you find that your design calls for large amounts of data or high levels of API calls, that's probably a case where we should be talking with you about alternatives. For example, since you're talking about having an external server already (to run the CRON job), one possibility is that you just make a call to it in Cloud Script to record the data you need about the time of the game move. Then, you could have a process running locally which constantly checks to see if it's time to send a Push Notification, while monitoring to ensure that the API call rate doesn't exceed some pre-defined threshold. You could also use the Segment integration to send this data as events wherever you need them.

And, of course, if it's for a title which has funding, working out a new feature with us via a custom engineering contract is always an option.

Brendan

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Write an Answer

Hint: Notify or tag a user in this post by typing @username.

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.