polling for chat and list of connected players

A backward incompatible change was introduced in the poll method to support chat and online players notification. The client was upgraded to the new protocol. The stable-1.0 branch was created to maintain the 1.x series.

The primary motivation behind these changes is to allow the client to be notified about the list of connected players and receive messages (chat) from other playres.

poll and state architecture

The poll architecture of cardstories 1.x was specialized : the client could only poll for a specific game state or for the lobby of a given player. It was not fit to update the list of connected players or even to maintain a simple chat. The architecture was modified to allow the client to poll a list of objects instead of a single one. For instance, as demonstrated in the test_services.py code, it could ask for the state of the game and the lobby and the state of a specific plugin as follows:

        #
        # type = ['game','lobby','plugin']
        #
        state = yield self.service.state({ 'type': ['game','lobby','plugin'],
                                           'modified': [0],
                                           'game_id': [game['game_id']],
                                           'in_progress': ['true'],
                                           'player_id': [owner_id] })
        self.assertEquals(state[0]['type'], 'game')
        self.assertEquals(state[1]['type'], 'lobby')
        self.assertEquals(state[2]['type'], 'plugin')

After the client displays the state, it should poll the server to be notified when the state of the displayed objects change, as follows:

http://localhost/resource?action=poll&type=game&game_id=38&type=example&modified=13209840

It will return when either something happens in the game with game_id=38 or when something happens in the example plugin.

server implementation

A backward incompatible poll function was implemented to enable the client to chose multiple objects to poll. It can now be a list as specified in the type argument of the query string. type=game and/or type=lobby. The type can also be a plugin name such as type=example and it will expect the corresponding plugin to notify the client when something happens to it. The poll will return when the first object of interest changes state. The state method (action=state) is implemented and returns the state of the game as a list of object states.

    @defer.inlineCallbacks
    def state(self, args):
        self.required(args, 'state', 'type', 'modified')
        states = []
        if 'game' in args['type']:
            game_args = {'action': 'game',
                         'game_id': args['game_id'] }
            if args.has_key('player_id'):
                game_args['player_id'] = args['player_id']
            game = yield self.game(game_args)
            game['type'] = 'game'
            states.append(game)
        if 'lobby' in args['type']:
            lobby = yield self.lobby({'action': 'lobby',
                                      'in_progress': args['in_progress'],
                                      'my': args.get('my', ['true']),
                                      'player_id': args['player_id']})
            lobby['type'] = 'lobby'
            states.append(lobby)
        for plugin in self.pollable_plugins:
            if plugin.name() in args['type']:
                state = yield plugin.state(args)
                state['type'] = plugin.name()
                state['modified'] = plugin.get_modified()
                states.append(state)
        defer.returnValue(states)

Each object is required to set the type key to match the type key used in the incoming state method arguments so that the client can figure out where it comes from. The type=game describes a specific game, the type=lobby describes a lobby and the type=example describes the state of the example plugin. The example plugin was updated to demonstrate how poll / state can be used from the perspective of a plugin author.

    #
    # pollable:
    #
    # Demonstrate how the plugin can notify the clients when
    # its state changes.
    #
    def count(self):
        self.counter = 0
        def incr():
            self.counter += 1
            #
            # The argument of the touch method must be a map.
            # It is implemented by the pollable class and
            # the 'modified' key will be set to the unix timestamp
            # of the current time. Each client will receive a
            # copy of the map, in a JSON object.
            #
            self.touch({'info': True}) # notify the clients
        reactor.callLater(0.01, incr)

    #
    # pollable:
    #
    # The state method is called when a client requires the action=state
    # of the server and adds type= (type=example in this
    # example) to the query string. The args argument is a map with
    # the following keys:
    #
    # args['modified'] = unix timestamp
    #  return only the data that has changed after timestamp
    #  argument. It is not required to return only
    #  the delta and the method can return the full state of the plugin.
    #
    # In the context of a chat plug, the state method would be expected to
    # return the lines written since the last call to the state method.
    #
    def state(self, args):
        args['modified'][0] # unix timestamp
        state = {}
        state['counter'] = self.counter
        return defer.succeed(state)

client upgrade and

The javascript client was upgraded to use the new protocol. It does not currently take advantage of it. The refresh_lobby and game functions should be made to use a common function implementing the new protocol and taking advantage of it. Such a function would dispatch the list of results returned by the state action to functions according to the type entry found in each of them.