aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/client/channel.py
blob: 2063272f46c1c70b21807179469dc077aec23c8f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# ==============================================================================
# Copyright (C) 2019 - Philip Paquette, Steven Bocco
#
#  This program is free software: you can redistribute it and/or modify it under
#  the terms of the GNU Affero General Public License as published by the Free
#  Software Foundation, either version 3 of the License, or (at your option) any
#  later version.
#
#  This program is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#  FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
#  details.
#
#  You should have received a copy of the GNU Affero General Public License along
#  with this program.  If not, see <https://www.gnu.org/licenses/>.
# ==============================================================================
""" Channel

    - The channel object represents an authenticated connection over a socket.
    - It has a token that it sends with every request to authenticate itself.
"""
import logging

from tornado import gen

from diplomacy.communication import requests
from diplomacy.utils import strings, common

LOGGER = logging.getLogger(__name__)

def _req_fn(request_class, local_req_fn=None, **request_args):
    """ Create channel request method that sends request with channel token.

        :param request_class: class of request to send with channel request method.
        :param local_req_fn: (optional) Channel method to use locally to try retrieving a data
            instead of sending a request. If provided, local_req_fn is called with request args:

            - if it returns anything else than None, then returned data is returned by channel request method.
            - else, request class is still sent and channel request method follows standard path
              (request sent, response received, response handler called and final handler result returned).

        :param request_args: arguments to pass to request class to create the request object.
        :return: a Channel method.
    """
    str_params = (', '.join('%s=%s' % (key, common.to_string(value))
                            for (key, value) in sorted(request_args.items()))) if request_args else ''

    @gen.coroutine
    def func(self, game=None, **kwargs):
        """ Send an instance of request_class with given kwargs and game object.
            :param self: Channel object who sends the request.
            :param game: (optional) a NetworkGame object (required for game requests).
            :param kwargs: request arguments.
            :return: Data returned after response is received and handled by associated response manager.
                See module diplomacy.client.response_managers about responses management.
            :type game: diplomacy.client.network_game.NetworkGame
        """
        kwargs.update(request_args)
        if request_class.level == strings.GAME:
            assert game is not None
            kwargs[strings.TOKEN] = self.token
            kwargs[strings.GAME_ID] = game.game_id
            kwargs[strings.GAME_ROLE] = game.role
            kwargs[strings.PHASE] = game.current_short_phase
        else:
            assert game is None
            if request_class.level == strings.CHANNEL:
                kwargs[strings.TOKEN] = self.token
        if local_req_fn is not None:
            local_ret = local_req_fn(self, **kwargs)
            if local_ret is not None:
                return local_ret
        request = request_class(**kwargs)
        return (yield self.connection.send(request, game))

    func.__request_name__ = request_class.__name__
    func.__request_params__ = str_params
    func.__doc__ = """
            Send request :class:`.%(request_name)s`%(with_params)s``kwargs``.
            Return response data returned by server for this request.
            See :class:`.%(request_name)s` about request parameters and response.
                """ % {'request_name': request_class.__name__,
                       'with_params': ' with forced parameters ``(%s)`` and additional request parameters '
                                      % str_params if request_args else ' with request parameters '}
    return func

class Channel:
    """ Channel - Represents an authenticated connection over a physical socket """
    # pylint: disable=too-few-public-methods
    __slots__ = ['connection', 'token', 'game_id_to_instances', '__weakref__']

    def __init__(self, connection, token):
        """ Initialize a channel.

            Properties:

            - **connection**: :class:`.Connection` object from which this channel originated.
            - **token**: Channel token, used to identify channel on server.
            - **game_id_to_instances**: Dictionary mapping a game ID to :class:`.NetworkGame` objects loaded for this
              game. Each :class:`.NetworkGame` has a specific role, which is either an observer role, an omniscient
              role, or a power (player) role. Network games for a specific game ID are managed within a
              :class:`.GameInstancesSet`, which makes sure that there will be at most 1 :class:`.NetworkGame` instance
              per possible role.

            :param connection: a Connection object.
            :param token: Channel token.
            :type connection: diplomacy.client.connection.Connection
            :type token: str
        """
        self.connection = connection
        self.token = token
        self.game_id_to_instances = {}  # {game id => GameInstances}

    def _local_join_game(self, **kwargs):
        """ Look for a local game with given kwargs intended to be used to build a JoinGame request.
            Return None if no local game found, else local game found.
            Game is identified with game ID **(required)** and power name *(optional)*.
            If power name is None, we look for a "special" game (observer or omniscient game)
            loaded locally. Note that there is at most 1 special game per (channel + game ID)
            couple: either observer or omniscient, not both.
        """
        game_id = kwargs[strings.GAME_ID]
        power_name = kwargs.get(strings.POWER_NAME, None)
        if game_id in self.game_id_to_instances:
            if power_name is not None:
                return self.game_id_to_instances[game_id].get(power_name)
            return self.game_id_to_instances[game_id].get_special()
        return None

    # ===================
    # Public channel API.
    # ===================

    create_game = _req_fn(requests.CreateGame)
    get_available_maps = _req_fn(requests.GetAvailableMaps)
    get_playable_powers = _req_fn(requests.GetPlayablePowers)
    join_game = _req_fn(requests.JoinGame, local_req_fn=_local_join_game)
    join_powers = _req_fn(requests.JoinPowers)
    list_games = _req_fn(requests.ListGames)
    get_games_info = _req_fn(requests.GetGamesInfo)
    get_dummy_waiting_powers = _req_fn(requests.GetDummyWaitingPowers)

    # User Account API.
    delete_account = _req_fn(requests.DeleteAccount)
    logout = _req_fn(requests.Logout)

    # Admin / Moderator API.
    make_omniscient = _req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.PROMOTE)
    remove_omniscient = _req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.DEMOTE)
    promote_administrator = _req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.PROMOTE)
    demote_administrator = _req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.DEMOTE)
    promote_moderator = _req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.PROMOTE)
    demote_moderator = _req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.DEMOTE)

    # ====================================================================
    # Game API. Intended to be called by NetworkGame object, not directly.
    # ====================================================================

    _get_phase_history = _req_fn(requests.GetPhaseHistory)
    _leave_game = _req_fn(requests.LeaveGame)
    _send_game_message = _req_fn(requests.SendGameMessage)
    _set_orders = _req_fn(requests.SetOrders)

    _clear_centers = _req_fn(requests.ClearCenters)
    _clear_orders = _req_fn(requests.ClearOrders)
    _clear_units = _req_fn(requests.ClearUnits)

    _wait = _req_fn(requests.SetWaitFlag, wait=True)
    _no_wait = _req_fn(requests.SetWaitFlag, wait=False)
    _vote = _req_fn(requests.Vote)
    _save = _req_fn(requests.SaveGame)
    _synchronize = _req_fn(requests.Synchronize)

    # Admin / Moderator API.
    _delete_game = _req_fn(requests.DeleteGame)
    _kick_powers = _req_fn(requests.SetDummyPowers)
    _set_state = _req_fn(requests.SetGameState)
    _process = _req_fn(requests.ProcessGame)
    _query_schedule = _req_fn(requests.QuerySchedule)
    _start = _req_fn(requests.SetGameStatus, status=strings.ACTIVE)
    _pause = _req_fn(requests.SetGameStatus, status=strings.PAUSED)
    _resume = _req_fn(requests.SetGameStatus, status=strings.ACTIVE)
    _cancel = _req_fn(requests.SetGameStatus, status=strings.CANCELED)
    _draw = _req_fn(requests.SetGameStatus, status=strings.COMPLETED)