Protocol

Workflows

General

Authentication

To authenticate, a client sends an AsyncAuthRequest to the server. The server can answer with:

blockdiag Client Server AsyncAuthRequest AsyncConnectionErrorRequest AsyncConnectedRequest error connected

When successful, the client will receive the full Player and a Session.

The client authenticates the end-user to the server with an Asmodee.net API OAuth2 access token.

Authentication with an OAuth2 access token

The client should authenticate beforehand through the Asmodee.net API, and request an OAuth2 access token.

Then the client will fill the name field with the user login name, and the PartnerToken sessionToken field with the OAuth2 access token.

Logging out

For the user to log out of the server, the client should send an AsyncDisconnectRequest. After this, all requests coming from the client will be rejected until the next AsyncAuthRequest.

The server keeps track of successful connections, by binding the device to the DoW account. When the player logs out, the DoW account is still linked to the device, so the server can send relevant push notifications. If the client logs in with a different DoW account, this account will be bound to the same device, along with the previous account. This means that a given device might receive notifications for a different user than the one currently logged in. Thus, the client should unlink the device when connecting with a different DoW account, by sending an AsyncUnlinkDeviceRequest.

Note

You should keep track on the device of the links between the DoW account and the Session (if known), so it is easy to check if the account changed or if the player just wants to reconnect with the same account.

blockdiag Client Server AsyncAuthRequest(playerA, sessionA, currentDevice) AsyncConnectedRequest(sessionA, playerA) AsyncUnlinkDeviceRequest(currentDevice) AsyncDeviceUnlinkedRequest(currentDevice) AsyncDisconnectRequest AsyncAuthRequest(playerB, sessionB, currentDevice) AsyncConnectedRequest(sessionB, playerB) player A logs out notifications for A will not be received on this device anymore player B logs in notifications for B will be received on this device

Heartbeat

The client must send regularly PingRequest to signal the server it is still connected. The server will respond with the exact same PingRequest. This allows also the client to notice that the connection with the Scalable Server has been dropped (a TCP timeout can take a long time).

The PingRequest contains a timestamp field where you can write the local timestamp. Since the server sends back the same request, in return you’ll get the same timestamp you sent, this allows to compute the time distance between the client and the server.

Current Games Information

It is possible at any time to get access to the list of games the Player is participating in. It is also possible to get information about only one game at a time.

To do so the client needs to send a WhatsNewPussycatRequest, to which the server will respond with a GameStatusReportRequest.

The returned status is a list (possibly of zero or one element) of StatusReport. These StatusReport contains all information about a given game, including (but not limited to):

  • Player list

  • Status

  • Game data, and/or summary game data

  • Active player

  • Player clocks

Invitation

The simplest way to create a game for a player is to create an Invitation. For a Player to create an invitation, the client sends an EngageGameWithFriendsRequest with the list of friends id (this list can’t be empty). It is possible to specify robots by adding the special id 0 to the list (a list containing only robots will result in an error).

Upon reception of that request, the server will send a GameCreatedRequest to the sender and all the invitees (if they’re not present a Push Notification will be sent). Note that the invitees have to accept or decline the invitation by sending back an AnswerInvitationRequest which is confirmed by the server with an InvitationAnsweredRequest. If any of the invitees declines the invitation the game is aborted (1), if all accept the invitation the game starts (2).

blockdiag Creator Server Invitees EngageGameWithFriends GameCreatedRequest(invited_by=1) GameCreatedRequest(invited_by=1) AnswerInvitationRequest(accept=false) InvitationAnsweredRequest AnswerInvitationRequest(accept=true) InvitationAnsweredRequest game is created, asking for answers from invitees (1) an invitee declines game becomes ABORTED (2) all invitees accept game starts

Note

It is considered best practice to send a SwitchedToGameRequest before positive acceptation in order to signal that the player is ready in the waiting room.

To quit an invitation there are two possibilities:

  1. if the inviter wants to quit, she has to send a GameForfeitRequest and this will abort the whole invitation

  2. invitees will have to decline the invitation by sending an InvitationAnsweredRequest with parameter accept=false

Presence

To monitor presence of other players, the client needs to send their Asmodee.net player id with a RegisterPresenceRequest and subscribe to the presence stream with a SubscribePresenceServiceRequest.

Once subscribed, anytime the presence of one of the monitored player changes, an AsyncBuddyPresencePartialUpdateRequest is sent with the state change. Note that presence events are batched, so an AsyncBuddyPresencePartialUpdateRequest can contain more than one player presence information.

To be noted that while in a game, the opponents presence is automatically monitored, and their presence change is sent with PlayerPresenceUpdateRequest (there is no need to subscribe to the presence stream, but the client needs to switch to the correct game).

Switching to a Game

For in-game presence to work correctly, and also for synchronous games, it is necessary that the client sends a SwitchedToGameRequest whenever the client UI enters a specific game (for newly joined or created games, send it after receiving the GameCreatedRequest) or leaves completely a game. There is no response from the server, except that when switching into a game, the server will propagate this event in a PlayerPresenceUpdateRequest to the other players.

Buddy List

It is possible for a player to get the content of her Buddy and Ignored List, by sending an AsyncBuddyListRequest. In return the server will send back an AsyncBuddyListContentRequest containing the list of buddies.

To add a buddy, send an AsyncBuddyManagementRequest with the operation ADD, and the Asmodee.net player id to add. On return the server will send back an AsyncBuddyAddedRequest.

To remove a buddy, follow the same workflow with the operation REMOVE. The server will send back an AsyncBuddyRemovedRequest to confirm the operation.

Ignore List

As with the Buddy List, the client can manage the Player ignore list. To get access to the list, send an AsyncIgnoreListRequest, the server will return an AsyncIgnoreListContentRequest.

To manage the ignore list, send an AsyncIgnoreManagementRequest with the proper ADD or REMOVE operation, and the server will respond with either an AsyncIgnoreAddedRequest or an AsyncIgnoreRemovedRequest.

Server Statistics

It is possible to get some statistics the server keeps with AskServerStatisticsRequest, for instance to give an overview of the server state when entering the lobby: the number of games, the number of live players (connected), and the overall number of live players (connected and not connected).

The distinction between connected and not connected is subtle: we keep hosting live data for an unconnected player, waiting for her to connect back.

The server will respond a ServerStatisticsRequest with aforementioned data.

Note

The AskServerStatisticsRequest contains a subscribe parameter: when false (or not provided) the server will respond once, if true then the client will regularly receive statistics updates (until you re-send the request with subscribe=false).

blockdiag Client Server AskServerStatisticsRequest(subscribe=true) ServerStatisticsRequest(hosted_games=21, players=42, connected_players=27) ServerStatisticsRequest(hosted_games=22, players=44, connected_players=36) ServerStatisticsRequest(hosted_games=20, players=44, connected_players=31) time passes time passes

Chat

Players can send chat messages from the lobby or from a game with a MulticastChatRequest.

To send a chat visible to all the players in the lobby, use MulticastChatRequest without specifying a game_id (or with game_id = 0).

blockdiag Client Server Other Clients in lobby MulticastChatRequest (text='test') ClientChatRequest (text='test',sender=A) ClientChatRequest (text='test',sender=A)

To send a chat to all the players in a given game, use MulticastChatRequest and specify its game_id.

blockdiag Player A Server Player B MulticastChatRequest (game_id=12,text='test') ClientChatRequest (game_id=12,text='test',sender=A) ClientChatRequest (game_id=12,text='test',sender=A)

If the chat message text triggers the anti-profanity filter several times in a row, the sender will be muted for a certain time, and the workflow becomes:

blockdiag Player A Server Player B MulticastChatRequest (game_id=12,text='profanity') ClientChatBlockedRequest (game_id=12,text='<bip>',sender=A,muted=true) ClientChatBlockedRequest (game_id=12,text='<bip>',sender=A,muted=true) MulticastChatRequest (game_id=12,text='message')

and no chat message is sent to the other players.

When sending chat requests you can also specify some players in the field recipient_ids. This is useful if you want to filter who receives the message (for instance, to implement private messaging so players can share a private game’s password). In the lobby, recipient_ids must contain global ids. In a game, recipient_ids must contain local ids.

  • If recipient_ids is empty, the message will be sent to all players (of the lobby, or of the game, according to the context).

  • If recipient_ids is filled, only these player ids will receive the message (and the sender will always receive it back).

blockdiag Player A Server Player B Player C MulticastChatRequest (game_id=12, text='test', recipients_ids=[idB]) ClientChatRequest (game_id=12, text='test', recipients_ids=[idB], sender=A) ClientChatRequest (game_id=12, text='test', recipients_ids=[idB], sender=A) ClientChatRequest (game_id=12, text='test', recipients_ids=[idB], sender=A)

Chat context

game_id

recipients_ids

Lobby

none or 0

global ids

In game

the game id

local ids (in the game)

Note

It is possible to get the history of the previous chat messages by sending a GetChatHistoryRequest (by specifying a game_id, or with id 0 / no id for the lobby). The server will send back a ClientChatHistoryRequest with the latest previously exchanged messages. Note that the chat history will never display private messages.

Chat codes

The server supports message codes, useful for implementing custom messages like pre-made quickchat messages, commands, emojis, special animation in the lobby…

Just fill the code integer parameter and it will be relayed. Please be aware that chat codes between 0 and 255 are reserved by the server and are not free to use. Provide a higher value. The code 0 is the “dummy code” and will behave the same way as if no code was provided.

You also have to provide a non-empty text (matching the message in default game locale) for older clients to display the message correctly, and for debugging purpose. The text will still pass through the profanity filter as usual.

Currently implemented chat codes in the Scalable Server:

Chat code

Description

1

Welcome message (in game)

2

Inappropriate message from a player

3

Advertising /report command feature

4

“Thanks for your report”

5

Report cooldown (players cannot spam report)

6

Help (list of commands)

7

Lobby statistics (command /statistics)

8

In-game statistics (command /statistics)

9

Welcome message (in lobby)

10 ~ 255

Reserved for future use

Chat commands

The Scalable Server offers a few commands, to be issued from the chat like a regular message. Currently, there are:

  • /help: displays the list of all supported commands

  • /report toto: to report the user toto (please note that no check is done server-side regarding the username)

  • /statistics: displays statistics on the lobby (number of players, number of open games) or the current game (number of active players, of forfeiters…)

Lobby

Entering

To be part of the lobby one needs to enter it by sending an EnterLobbyRequest, the server will return LobbyEnteredRequest. If the player was already in the lobby, the server will return an ErrorRequest (PLAYER_ALREADY_IN_LOBBY).

Exiting

To exit the lobby, send an ExitLobbyRequest, the server will return a LobbyExitedRequest. Note that upon joining a game (or creating a game), when that game starts, the player is automatically exited from the lobby.

Player List

When entering the lobby, the client will automatically and regularly receive LobbyPlayerListRequest. This request contains the list of the players connected in the lobby.

Note

The embedded LobbyPlayerListRequest.PlayerList is in fact stored as a binary string, result of the Zlib Deflate compression. The protobuf C++ library contains utilities to uncompress this format, other languages usually have this feature in their standard library.

The player list contains instances of SmallPlayer which are not regular Player. Those contain less information than regular Player instances. If more information is needed, please see the Player Information workflow.

Open Game List

Like with the Player List workflow, the client once in the lobby will regularly receive a LobbyGameListRequest, containing the list of the open games and their details.

Note

The embedded GameList is in fact stored as a binary string, result of the Zlib Deflate compression. The protobuf C++ library contains utilities to uncompress this format, other languages usually have this feature in their standard library.

Player Information

To get access to another player information, just send an AskPlayerInfoRequest with the Asmodee.net player id. In return the server will send a LobbyPlayerInfoRequest containing a Player instance with the information needed.

Joining an Open Game

To join an open game, the client must send a LobbyJoinGameRequest:

blockdiag Client Server Other Players LobbyJoinGameRequest LobbyNewPlayerRequest LobbyNewPlayerRequest

If the join is denied (see LobbyJoinDeniedRequest.JoinError for the list of possible cause), a LobbyJoinDeniedRequest is sent back to the sender, the other players of the game are not informed.

To join a Private Game, its password needs to be provided.

Warning

It is the client responsibility to ensure the client compliance with the rules engine before joining by checking the rules_engine_version field of GameConfiguration (for more information, see Manage online versioning).

Creating an Open Game

To create an open game, the client sends a LobbyCreateGameRequest, with the correct GameConfiguration and possibly initial state and summary state.

The server can answer with an ErrorRequest (TOO_MANY_OFFERS) if the creator has too many games in progress, or with a LobbyGameCreatedRequest if it succeeded.

The Game UI should then show a waiting screen until either the creator aborts by leaving or enough players join this open game.

Leaving an Open Game

Until the game is started it is possible for a player to leave an open game, by sending a LobbyLeaveGameRequest:

blockdiag Client Server Other Players LobbyLeaveGameRequest LobbyPlayerLeftGameRequest LobbyPlayerLeftGameRequest

Warning

Note that if the game creator leaves the game while it is still open, there would be no way to start the game as it is her responsibility. Thus, the open game will abort.

Starting the Game

A game starts when the correct number of player have joined it. In this case all players will receive a GameCreatedRequest.

The game creator can decide to start the game before the game is complete (i.e. before max_players players have joined). For this to work, the client must set the appropriate min_players and max_players fields of GameConfiguration during game creation.

When the game creator (and only the game creator) thinks there are enough players she can send a LobbyStartGameRequest which will either:

  1. start the game, the rest of the protocol is the same as when a game is complete.

  2. return an error LobbyStartGameDeniedRequest with a dedicated cause (e.g.: if the game configuration minimum player is not reached). The complete error list is LobbyStartGameDeniedRequest.StartError.

blockdiag Client Server Other Players LobbyStartGameRequest GameCreatedRequest GameCreatedRequest LobbyStartGameDeniedRequest Game configuration reached (1) Start denied (2)

Note

When the open game is complete (all player slots are full) or received an “early start” from the creator, the game will really start automatically and all players will be exited from the lobby.

Observable Game List

While in the lobby, it is optionally possible to subscribe to the list of observable games. This list contains games that have been started and for which you can observe what the players are doing.

Subscribing to the list update is done by sending a SubscribeToObservableGameListRequest with subscribe to true to the Scalable Server. The server will reply with a SubscribedToObservableGameListRequest where subscribed is true.

To unsubscribe, one needs to send a SubscribeToObservableGameListRequest with subscribe to false. The server will respond with a SubscribedToObservableGameListRequest where subscribed is false.

When subscribed the client will receive a stream of ObservableGameListRequest containing the list of games that can be observed. To be noted that this list doesn’t contain live data of the games. It is required to start observing a game to get the real game state.

Observing a Game

To observe a game, just send a StartObserveGameRequest with the game_id of the game to observe. To stop observing send a StopObserveGameRequest with the game_id of the game to stop observing. The server answers to these two requests with a GameObservedRequest which might either contain an error status or OK and the full game StatusReport.

Note that it isn’t possible to observe:

  • a game you’re playing in

  • a private game

  • a non-observable game

  • an observable game restricting observers to buddies if you’re not a buddy of the creator

  • if you’re not in the lobby

  • if the game is OVER or NOT_STARTED

  • if the game doesn’t exist

All those cases generates a GameObservedRequest with the appropriate error status.

In order for a player to watch a game outcome, just after forfeiting or timing out, the player can start observing this game by sending StartObserveGameRequest with the game id (this is irrespective of the observability properties of the game). The client GUI should list games in which the player has timed out or forfeited (see WhatsNewPussycatRequest) as observable games along with the list of games the player is active in.

In game

The Scalable Server doesn’t implement any specific game logic, it can only accommodate turn-by-turn games. It’s up to the client to implement the correct game workflow, the Scalable Server will just provide the communication layer and persistence for the game state.

This turn workflow has been designed so that the Game State is unique and is controlled only by one Client at a time, preventing state corruption. The server decides which client can modify the Game State, based on what the previous player told it. The Game State modifications are fully atomic, and the client must send the whole Game State.

Standard Game Turn

A standard game turn is a sequence of the following events:

  1. Server sends an ActionRequiredRequest to the current player’s client with the current state, and some other information useful to the client (like turn_index, current player_clock…)

  2. Current player sends back a CommitActionRequest containing a new state in next_state, and the list of the next players in sequence in next_players (it can start with the same current player to play again). It can also send a next_summary_data if needed.

  3. Server acknowledges the commit with an ActionCommitedRequest. The client can check that the turn_index is correct. The server can also send an error at this stage (see below).

  4. Server decides who’s playing next based on the list of next players sent in the CommitActionRequest, and starts back to point 1.

Here’s what it gives graphically with a three players game:

blockdiag Player A Player B Player C Server ActionRequiredRequest(state=<> turn_index=1) CommitActionRequest(state=A, next=[B, C, A], turn_index=1) ActionCommitedRequest(turn_index=2) ActionRequiredRequest(state=A, turn_index=2) CommitActionRequest(state=B, next=[C, A, B], turn_index=2) ActionCommitedRequest(turn_index=3) ActionRequiredRequest(state=B, turn_index=3) CommitActionRequest(state=C, next=[C, B, A], turn_index=4) ActionCommitedRequest(turn_ind ex=4) ActionRequiredRequest(state=C, turn_index=4) CommitActionRequest(state=C1, next=[A, B, C] ,turn_index=5) ActionCommitedRequest(turn_ind ex=5) 1st Turn 2nd Turn 3rd Turn 4th Turn

Notice how in the above example Player C is playing again in the 4th turn.

Note that the next_players is an array. This allows the server to know what would a possible turn look like, this also allows the server to select another player in case of robot hotswap.

Warning

We recommend to put all possible players in the array next_players in order to support robot hotswap and avoid unwanted abort of the game.

Also note that if the broadcast boolean parameter is set to true, the new game state will be broadcast to other active players and observers. This avoids using the Multicast workflow.

blockdiag Player A Player B Observer Server CommitActionRequest(next_state="toto", broadcast="true") ActionCommitedRequest GameStateUpdatedRequest(state="toto") GameStateUpdatedRequest(state= "toto")
Pausing Clock after commit

Warning

This feature is experimental, protocol might change in the future.

Some games display the previous players turns animations at the start of the active player turn. While those animations run on the client, the active player clock is ticking on the server. Players might complain that their time has been consumed while they weren’t able to play, which can be perceived as unfair.

In order to mitigate this feeling, it is possible to specify a short pause time for the active player clock for this specific purpose. The clock will not be decreased for this delay, and then it will resume and tick normally. You have to fill the pause_time member of CommitActionRequest with the duration of the pause (in seconds) for the immediate next player. This time is given back in ActionCommitedRequest, and transmitted to the player through ActionRequiredRequest. Clock-related messages will be sent at appropriate moments: ClockPausedRequest when the clock is effectively paused (beginning of the turn), and ClockResumedRequest (providing clocks status) when the pause time is consumed. If for some reason, the player commits action before the pause is over, the server will cancel the resume order as it makes no sense.

If the provided pause time is zero, no pause is done and the turn follows the regular protocol of a Standard Game Turn.

blockdiag Player A Player B Server A is not paused but get the information that clock should be stopped also contains other clocks B is not paused but get the information that clock should be stopped CommitActionRequest(next=[B,A], pause_time=3) ActionCommitedRequest(pause_time=3) ActionRequiredRequest(pause_ti me=3) ClockPausedRequest() ClockPausedRequest() ClockResumedRequest(clock[A].clock.paused=false, clock[A].clock.armed=false) ClockResumedRequest(clock[B].c lock.paused=false, clock[B].clock.armed=true) CommitActionRequest(next=[A,B] , pause_time=3) ActionCommitedRequest(pause_ti me=3) ActionRequiredRequest(pause_time=3) ClockPausedRequest() ClockPausedRequest() CommitActionRequest(next=[B,A], pause_time=3) ActionCommitedRequest(pause_time=0) ActionRequiredRequest(pause_ti me=0) 3 seconds pass, the clock for B is not decreased everybody receives information that clock should resume pause time is exhausted, clock B has been resumed, turn continues as normal played but pause was not over: will not "resume" the clock pause time is 0: no pause

Note

Please note that a maximum duration for the pause is set in configuration, giving a pause time that breaks the limit will result in an error.

Warning

The clock might resume at a moment where the player was not on the app. When the player comes back, she will notice that the clock is not paused. In fact, the “pause time” was already done server-side but the player just was not there to witness it.

Idle timer

Warning

This feature is experimental, protocol might change in the future.

The player clock ensures no malicious player can lock the game by not playing. Sometimes this might not be enough, as waiting for a whole player clock to expire can be quite long and cause frustration for other players.

Besides the player clock, the Scalable Server offers an optional idle timer. Regular updates of the timer progress will be broadcast with PlayerIdleProgressRequest, filled with the array of players currently idle and the percentage of total time currently exhausted (at 50%, 75% and 100%). When exhausted, the server will ask a robot to play for the currently idle players, as if they left, and the game will continue as normal (players are not kicked out of the game).

To enable the idle timer at game creation, fill the idle_time field of GameConfiguration with a positive number of seconds. If the provided idle_time is zero or negative, the timer will not be used and the turn follows the regular protocol of a Standard Game Turn.

Warning

We strongly advise to scale the idle_time duration with the total player clock, so that longer games will have a longer idle time, and shorter games will have a shorter idle time.

Note

When idle timer is enabled at game creation, one can override the timer for the next turn in order to deal with asymetrical games or non linear thinking phase through idle_time in CommitActionRequest.

blockdiag Player A Player B Server LobbyCreateGameRequest(GameConfiguration(idle_time=40)) ActionRequiredRequest() PlayerIdleProgressRequest(progress=50, player_ids=[A]) PlayerIdleProgressRequest(prog ress=50, player_ids=[A]) PlayerIdleProgressRequest(progress=75, player_ids=[A]) PlayerIdleProgressRequest(prog ress=75, player_ids=[A]) PlayerIdleProgressRequest(progress=100, player_ids=[A]) PlayerIdleProgressRequest(prog ress=100, player_ids=[A]) PlayerTimeoutRequest(offender_ id=[A], status=IDLE) CommitActionRequest(next=[A,B] ) ActionCommitedRequest() ActionRequiredRequest() all players join, the game starts player B should play, starting the idle timer immediately 20 seconds passed, 50% of the total idle time 30 seconds passed, 75% of the total idle time 40 seconds passed, 100% of the total idle time B plays as a robot for A, game continues as normal a new idle timer begins...

Warning

Even though we asked a robot to play, the original player still has ability to commit her action, while an other player will also try to commit action for her (as a robot). Depending on who shot first, the player or the robot handler will get an ErrorRequest with NOT_YOUR_TURN error. The game will continue as normal, as either way someone played for this turn.

Note

When using a pause_time at commit, the idle timer will start only when the pause ends.

Errors

When a client commits data to the server, the following errors can be returned:

Error Type

Meaning

NOT_YOUR_TURN

player tried to play, server thinks she’s not the current player

UNKNOWN_PLAYER

next contains an invalid player

player sending this commit is not part of this game

UNKNOWN_GAME

game_id is not part of on-going games of the player

YOU_FORFEITED

this player forfeited earlier, and thus can’t play anymore

YOU_RAN_OUT_OF_TIME

this player exhausted her player clock, and thus can’t play anymore

YOU_LEFT

this player has left a synchronous game, she needs to resume the game first

INDEX_CONFLICT

this player provided a turn index, but it does not match the server’s own turn index

BAD_REQUEST

provided pause_time is incorrect

simultaneous_players are invalid (see next)

interruptible configuration is invalid (see next)

any other problem the server detected

Simultaneous Game Turn

Simultaneous turns allow to require multiple independent players actions during the same turn.

To enter a simultaneous turn, one must send a CommitActionRequest with next_simultaneous as true, and provide players required to act in simultaneous_players array. Once the action is validated, the game will enter in simultaneous turn and send UserDataUpdateRequiredRequest to all provided players (if present, otherwise PlayerTimeoutRequest is sent).

For players to send data, use UpdateUserDataRequest. This should also be used when playing for a robot. The Scalable Server will send back UserDataUpdatedRequest for confirmation.

Once all required players provided their UserData, the next player will be selected to merge all the actions. She receives ActionRequiredRequest with user_data populated with previously described requests.

Warning

In case of next player is an invited robot, a PlayerTimeoutRequest is sent.

Note

merge turn is seen as an administrative turn and will not update the next array of players.

She sends a CommitActionRequest with the new state. If valid, the simultaneous turn is completed (after sending ActionCommitedRequest for confirmation). According to commit’s next_simultaneous member, the next turn may be simultaneous or not…

blockdiag Player A Player B Server C is a robot CommitActionRequest(next_simultaneous=true, next_state="toto", simultaneous_players=[A, B, C]) ActionCommitedRequest UserDataUpdateRequiredRequest(state="toto") UserDataUpdateRequiredRequest(state="toto") PlayerTimeoutRequest(offender_id=C) UpdateUserDataRequest(data="a") UserDataUpdatedRequest UpdateUserDataRequest(data="b") UserDataUpdatedRequest UpdateUserDataRequest(player_id=C, data="c") UserDataUpdatedRequest ActionRequiredRequest(user_data=[(A, "a"), (B, "b"), (C, "c")]) CommitActionRequest(next_state="abcfoobar", next_simultaneous=false) ActionCommitedRequest Entering simultaneous turn Waiting for players actions Player A plays the robot for C Ask a player to merge data

If the player never responds to the ActionRequiredRequest, we will send a PlayerTimeoutRequest following the same model as regular turns, except the Scalable Server will provide user_data required to perform the merge as a robot.

blockdiag Player A Player B Server ActionRequiredRequest(user_data=[(A, "a"), (B, "b"), (C, "c")]) PlayerTimeoutRequest(offender_id=1, user_data=[(A, "a"), (B, "b"), (C, "c")]) CommitActionRequest(next_state="abcfoobar") ActionCommitedRequest Ask a player to merge data Player A forfeits / times out / quits the game, we ask a live player

Also note that if the broadcast boolean parameter is set to true, the user data will be broadcast to other active players and observers. This can be useful for other clients to acknowledge that “Player A played and is waiting for you”.

blockdiag Player A Player B Observer Server UpdateUserData(user_data=["a"], broadcast=true) UserDataUpdatedRequest GameUserDataUpdatedRequest(player=A, user_data="a") GameUserDataUpdatedRequest(pla yer=A, user_data="a")

Warning

merger selection: the player which should perform the merge of all the user data is by default the first member of the current next array if present or an invited robot. If she’s not an invited robot and not present, the Scalable Server will select any other present player that was part of this simultaneous turn. To prevent waiting for the absent original merger player, the client logic must check and commit with the correct next array. If the simultaneous turn didn’t change the order of the next players, then the same next list should be used, in which case the original merger will be the next player to receive an ActionRequiredRequest.

Pausing Clock in a simultaneous turn

Warning

This feature is experimental, protocol might change in the future.

The same way clocks can be paused at the beginning of a standard turn, it is possible to pause the clocks at the beginning of a simultaneous turn. Besides setting next_simultaneous, you have to fill the pause_time member of CommitActionRequest with the duration of the pause (in seconds) for the simultaneous players. This time is given back in ActionCommitedRequest, and transmitted to the player through UserDataUpdateRequiredRequest. All clocks of simultaneous_players will be paused, but only one ClockPausedRequest and ClockResumedRequest will be sent (to all players). If for some reason, a player updates its user-data before the pause is over, other players will still be paused.

If the provided pause time is zero, no pause is done and the turn follows the regular protocol of a Simultaneous Game Turn.

blockdiag Player A Player B Player C Server C is not paused but will receive the information to freeze all clocks C is not resumed, just getting information for A and B CommitActionRequest(next=[B,A], pause_time=3, next_simultaneous=true, simultaneous_players=[A,B] ) ActionCommitedRequest(pause_time=3) UserDataUpdateRequiredRequest(pause_time=3) UserDataUpdateRequiredRequest(pause_time=3) ClockPausedRequest() ClockPausedRequest() ClockPausedRequest() ClockResumedRequest(clock[A].clock.paused=false clock[A].clock.armed=true clock[B].clock.paused=false clock[B].clock.armed=true ) ClockResumedRequest() ClockResumedRequest() UpdateUserDataRequest() UserDataUpdateRequired(pause_time=3) 3 seconds pass, clocks are not decreased pause time is exhausted, clocks A and B have been resumed, simturn continues as normal B played, its clock will stop. A did not play, its time will decrease

Note

Please note that a maximum duration for the pause is set in configuration, giving a pause time that breaks the limit will result in an error.

Warning

When the clock resumes, player might be absent and not see that the clock was paused. However, the time was indeed saved so globally this changes nothing.

Interruptions

When sending a CommitActionRequest, you can specify the action as interruptible: some other players will be asked by the Scalable Server if they want to interrupt the last action. For this, you have to specify the interruption_window_duration parameter with the duration you want the interruption to be possible for (in milliseconds).

Players set in interruption_player are the interrupters: they will have the given amount of time to tell the server about the interruption, by sending an InterruptActionRequest, providing interrupt_data (please keep them small as they are transmitted to all players). It is important to provide a correct turn_index so the server knows exactly which action is being interrupted, in case of race conditions during consecutive interruptions. The correct value is given by ActionCommitedRequest and GameStateUpdatedRequest.

Note

It is up to the game client’s to acknowledge the game is in an interruption state. After sending the CommitActionRequest, the server will send a GameStateUpdatedRequest to all players. This request relays the interruption_window_duration given at commit, so if it is non-zero, you should be in an interruption turn.

Once the time is exhausted, all players will receive an InterruptionOverRequest so they know the time is up. Any interruption received after the time delay will be discarded and return an InterruptActionErrorRequest. One player can also explicitly put a stop to the interruption phase by setting no_more_interruption.

A player will be selected to resolve the interruptions (as possibly there can be several interruptions) with an ActionRequiredRequest (or a PlayerTimeoutRequest if the merge should be performed by a robot). According to the result, the game will continue as normal.

blockdiag Player A Player B Player C Server CommitActionRequest(interruption_window_duration=3000, interruption_players=[B, C], next=[B, C, A]) GameStateUpdatedRequest(interruption_window_duration=3000) GameStateUpdatedRequest(interruption_window_duration=3000) GameStateUpdatedRequest(interr uption_window_duration=3000) InterruptActionRequest(turn_index=2, interruption_data="Not this time", turn=A) ActionInterruptedRequest(turn_index=2, interruption_data="Not this time", turn=A) ActionInterruptedRequest(turn_index=2, interruption_data="Not this time", turn=A) ActionInterruptedRequest(turn_ index=2, interruption_data="Not this ti me", turn=A) InterruptActionRequest(turn_in dex=2, interruption_data="I protest t oo", turn=A) ActionInterruptedRequest(turn_ index=2, interruption_data="I protest t oo", turn=A) ActionInterruptedRequest(turn_index=2, interruption_data="I protest too", turn=A) ActionInterruptedRequest(turn_index=2, interruption_data="I protest too", turn=A) InterruptionOverRequest(turn_index=2) InterruptionOverRequest(turn_index=2) InterruptionOverRequest(turn_i ndex=2) ActionRequiredRequest(turn_index=3, interruption_data=[(B, "Not this time"), (C, "I protest too")] Player A commits, asking for the next turn to be Interruptible Player B interrupts A Player C interrupts A Time is exhausted Ask Player B has been selected to merge interruption data

Warning

Please note that during all interruption steps, the time will be decreased from the original committer’s clock (in the diagram, Player A). During the merge action, the time is decreased from the merger’s clock (in the diagram, Player B).

We strongly recommend to hide all this protocol under a visual animation for timing. For instance, once I played an interruptible card, the game would trigger an animation whose duration corresponds to the interruption duration, so:

  • other players do not have an inactive screen waiting for several seconds (there is always something happening to keep player’s attention)

  • players have a visual indication that an interruption is possible, and how many time is left until the interruption window closes

It is up to the game’s client to set the interruption duration, thus we advise to use quite the same as animation duration. Also, the animation should start on all devices at the same time (or the closest possible) to minimize synchronization issues:

  • the interrupter could receive the message a little bit late, and the animation will still play as the interruption phase is over

  • the interruptee could start the animation too soon, so it thinks the interruption is over but the server still accepts interruption requests for a small period

To achieve this, we recommend triggering the animation not after sending CommitActionRequest but only after receiving GameStateUpdatedRequest. It is sent to all clients at the same time so only network latency will come in play.

Warning

Simultaneous turns do not support Interruptions. If you set both next_simultaneous and interruption_window_duration, the server will send an ErrorRequest.

Multicast

The MulticastDataRequest allows to send arbitrary information to the other present players in the game, for instance to refresh the UI.

Warning

Please note that this should not be used to relay the state change after an action. The correct way is to set the broadcast parameter of CommitActionRequest to true (or UpdateUserDataRequest in case of a simultaneous turn). The server will thus atomically send a GameStateUpdatedRequest exhibiting the new state (or user data) to other live players.

If, for whatever reason, you can’t use the broadcast parameter, you might have to use the MulticastDataRequest to send the new state data to other clients. However this is an anti-pattern and should be avoided:

  • the server will simply forward the information, it doesn’t know whether it’s related to state or not. It will not modify its state when receiving the request. You might inadvertently send data not compatible with the current state, resulting in a corrupted local state in the client.

  • MulticastDataRequest content is transient and will be lost for players leaving the game or in the rare event of a service restart, unlike the game state which is guaranteed available.

  • players may receive MulticastDataRequest before ActionCommitedRequest, possibly resulting in local conflicts.

  • worse, the next player may receive her ActionRequiredRequest before the MulticastDataRequest. The game engine would then compute the action to perform without having the last state.

Now that you know the danger of using the multicast to transmit the game state, here is how to use it to send arbitrary information. Any present player can send a MulticastDataRequest to the Scalable Server with some binary data and possibly a list of recipients_ids. The server will then send back a ClientDataRequest containing the binary data to all the recipients_ids. If recipients_ids is empty, all the present players will receive the request. Non-present players can also be notified by a push notification, if notification is filled-in.

blockdiag Player A Server Player B Player C MulticastDataRequest(data=A) ClientDataRequest(data=A) ClientDataRequest(data=A) ClientDataRequest(data=A)

Forfeit

At any time a player can decide to withdraw from the game (and lose it). She does so by sending a GameForfeitRequest. In return the server will send back a GameForfeitedRequest to all the present players in the game.

If the forfeiting player was the last active (i.e. all other players have already either forfeited, left or exhausted their Player Clock), then the game will Abort.

If the forfeiting player wasn’t the last remaining active player, the Scalable Server will do the necessary to replace it with a robot as explained in Robots. The Scalable Server will also send to all the present players a PlayerReplacedRequest so that they can display the hotswap information in the UI.

Player Timeout

When a player exhausts her own Player Clock, she can’t play anymore and is replaced by a robot, as explained in Robots section. The Scalable Server will also send to all the present players a PlayerReplacedRequest so that they can display the hotswap information in the UI.

Robots

Since the Scalable Server Game instance has no knowledge of the game engine, it can’t play robots directly. As such it asks one of the connected client to impersonate another player and play for her with an IA (which is what we call a robot).

This can happen for the following reasons:

In all those cases, at this player’s turn, the Scalable Server will select another client (preferentially from the ones still connected to the server) and ask it to play for this defecting player.

The game logic in the client thus must know how to play for a different player than the human using the UI.

The robot workflow works like this:

  1. Player A commits her action by sending a CommitActionRequest as usual

  2. server notices the next player (Player B) needs to be played by a robot

  3. if Player A is still present, the Server will send a PlayerTimeoutRequest to its client, for it to play for Player B

  4. Player A’s client sends a CommitActionRequest as if it was Player B

  5. server keeps going on with Player C

This is best illustrated with this sequence diagram:

blockdiag Player A Server Player C ActionRequiredRequest(state=<>, turn_index=1) CommitActionRequest(state=A, next=B, turn_index=1) ActionCommitedRequest(turn_index=2) PlayerTimeoutRequest(state=A, offender_id=B, turn_index=2) CommitActionRequest(state=B, next=[C, B], player_id=B, turn_index=2) ActionCommitedRequest(turn_index=3) ActionRequiredRequest(state=B, turn_index=3) CommitActionRequest(state=C, next=A, turn_index=4) ActionCommitedRequest(turn_index=4) 1st Turn 2nd Turn - B must be played by a robot 3rd Turn

If two or more players must be played by robots, the same process happens.

If Player A is not present anymore when the server needs a robot to play, it will select one of the other connected clients of this game in order of appearance in the last next_players array. If none are connected at this time, the server will select the next non-robot in order of appearance in the last next_players array and send her a push notification as if it was her turn to play.

Warning

We recommend to put all possible players in the array next_players.

Upon receiving this push notification, this client game logic (once the controlling human decides to play her turn) must check this game StatusReport, notice that turn and active_player are not the same (and active_player is this client’s player) and use a robot to play for the turn player.

Note

In a fully asynchronous game, any client can play for any player. To prevent any issue, the client that plays must check for the error NOT_YOUR_TURN after sending the CommitActionRequest. In case of error, it means another client already played, and the client should refresh its view of the StatusReport and possibly not play. As such, it is recommended to check the active_player value and only play when it matches the current player.

Game Over

At one point in the game, one of the player will trigger the end of game as implemented by the rules engine, this client will then send a GameOverRequest containing the ordering of the players in finalScores (including robots and timed out or forfeited players). Make sure the is_ranked is set to true if the Scalable Server needs to update the rankings of the players.

The game status will then change to OUTCOME, and all connected players will receive a GameOutcomeRequest containing ranking information (and the player’s scores). The clients will then display this in a final score screen.

This information can also be displayed from the StatusReport for asynchronous players.

All players will have to confirm the outcome by sending a GameOutcomeConfirmationRequest before the game can reach the OVER status, and ultimately disappear. For asynchronous clients, the game logic can know if this player already saw the final scores by checking the StatusReport outcome_not_seen array which contains the list of players that have not yet confirmed the outcome.

blockdiag Player A Server Player B ActionRequiredRequest(state=<>) CommitActionRequest(state=A,next=B) ActionCommitedRequest GameOverRequest(finalScores={A:[12,2],B:[23,1]} GameOutcomeRequest GameOutcomeRequest GameOutcomeConfirmationRequest GameOutcomeConfirmationRequest Last Turn Game Over Player A & B confirm

Warning

No commits or user data updates will be processed after a GameOverRequest is sent to the server, be sure to provide the final score and final state of the game (including final bonuses) in the GameOverRequest.

Note

In case of game over during a simultaneous turn, we recommend to send the GameOverRequest in place of a merge commit after receiving the ActionRequiredRequest.

Abort

A game can abort at any time if no more players can play (all timed out or all withdrawn) and will try to notify connected players (and observers) with a GameAbortedRequest containing the applied ranking penalties. The game status also changes to ABORTING and then after a while to ABORTED (to let the players see the game status with Current Games Information and then confirm with the message GameAbortedConfirmationRequest).

blockdiag Player A Server Player B Observer 1 GameAbortedRequest GameAbortedRequest GameAbortedRequest GameAbortedConfirmationRequest GameAbortedConfirmationRequest All players timeout or forfeited Aborted confirmation

Game Observation

As explained in Observation, it is possible for multiple players to observe what happens in a given game. First the client needs to start observing an observable game (from the observable game list).

Once in the list of observers, the client will receive all events a normal player would normally receive, including all ActionRequiredRequest and PlayerTimeoutRequest.

It is up to the client to display what happens in the game from this information and the game logic.

Player Clocks Status

It is possible for a client to fetch all the player clocks status. It is hard to maintain a synchronized clock while displaying the UI, so from time to time, it is necessary to refresh the player clocks from the server.

To do that, send a GetClocksRequest directed to a game, the server will send back a ClocksStatusRequest.

Note

The playing player clock is always sent in the ActionRequiredRequest, so at least every start of a player turn, her local clock can be synchronized.

Push Notifications

Definition

The Scalable Server is able to send native device push notification (to Steam users, Google android users and iOS/OSX users) when specific events happen:

  • an invitation is sent to a player

  • it is a player’s turn (or a robot turn)

  • end of a game

Prerequisites

To enable push notifications the Scalable Server needs to be provisioned with the correct configuration, as explained in the following table. The developer needs to contact the Asmodee.net Administrators to enable the push notification feature.

Platform

Material needed

iOS or OSX

bundle id of the application (com.asmodee-digital.xxx) from apple developper account provision profile

Google FCM

sender id

key

Steam

app id

api key

Client side integration

The Client, when authenticating a player by sending an AsyncAuthRequest, must provide a DeviceType structure containing:

  • the type of push system among (IOS, GCM, FCM, STEAM, OSX, IOS_SANDBOX, OSX_SANDBOX), see Devices.Type

  • the opaque token provided by the client operating system.

Apple allows to use a “sandbox” mode for Push Notifications, those are using their distinct codes because they use another endpoint of the APNS for developement/testing purpose (by default sandbox mode should be enabled).

For more information about receiving Push Notifications on iOS see Apple Push Notification Documentation.

For more information about receiving Push Notifications on Android see Firebase Cloud Messaging Documentation.

Note

GCM is deprecated and not supported anymore by Google. Developers should use FCM instead.

Format of a push notification

When an event triggering a push notification happens, the Scalable Server will notify the Push Notification platform matching the recipient device which will send to the client the following data (which models the data available in an iOS notification):

Key

Content

action-loc-key

id of the action

loc-key

id of the localized string in the application resources that will be displayed in the notification

loc-args

some arguments that depend on the notification type

async

notification payload

The payload is a protobuf serialized data structure of GameNotification that contains the game id, the event type, the player id of the recipient and the current turn index.

Apple Usage

On iOS, the client receives the notification as a dictionary. The above keys are used by the system to display the push notification if the app is not in foreground. For instance it will display a button whose string is searched in the app localized string whose key is the value of the action-loc-key. The text content of the notification will be derived from the interpolation with loc-args of a string coming from the app localized string whose key is loc-key. See Apple Push Notification Payload Key Reference for more information.

Android Usage

On Android, the system is less automatic. The Scalable Server only sends Data Messages.

The client will receive a RemoteMessage object and can get access to the Scalable Server payload through the getData call which will return a Map containing the keys shown above.

Once done, the client must build a notification (if in background) by looking at the loc-key string and interpolate it with loc-args as explained below, and display it as a notification (see NotificationCompat.Builder for this).

String interpolation

The interpolation of the loc-key matched string obeys to Apple format: every $@ will be replaced by one of the value of the loc-args array, for instance:

if ``loc-key`` give this string:
   "You've been invited by $@"
and ``log-args`` contains ``CaptnHook``, the result will be:
   "You've been invited by CaptnHook"

Type of notifications

Type

Key

Value

YOUR_TURN

Sent to the current player in a game

action_key

YOUR_TURN_ACTION

localized_key

YOUR_TURN

localized_args

none

YOUR_TURN_INVITE

Sent to the current player of an invitation game

action_key

YT_INVITE_ACTION

localized_key

YOUR_TURN_INVITE

localized_args

name of the inviter

YOUR_TURN_ROBOT

Sent to the player selected to play a robot for another player

action_key

YT_ROBOT_ACTION

localized_key

YOUR_TURN_ROBOT

localized_args

none

CONFIRM_INVITATION

Sent to a player that has been invited to a game so that she can confirm or deny the invitation

action_key

INVITATION_ACTION

localized_key

CONFIRM_INVITATION

localized_args

name of the inviter

INVITEE_ACCEPTED

Sent to a player to inform her that an invitee confirmed the invitation

action_key

INVITEE_ACCEPTED_ACTION

localized_key

INVITEE_ACCEPTED

localized_args

name of the invitee who accepted

INVITEE_DECLINED

Sent to a player to inform her that an invitee denied the invitation

action_key

INVITEE_DECLINED_ACTION

localized_key

INVITEE_DECLINED

localized_args

name of the invitee who declined

GAME_OVER

Sent to a player to inform her that the game ended. When all players saw the outcome, the game can be deleted

action_key

GAME_OVER_ACTION

localized_key

GAME_OVER

localized_args

none