multiplayer 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- multiplayer/IPClogging/__init__.py +42 -0
- multiplayer/IPClogging/echoing.py +34 -0
- multiplayer/IPClogging/server.py +338 -0
- multiplayer/IPClogging/test.py +71 -0
- multiplayer/__init__.py +52 -0
- multiplayer/client.py +531 -0
- multiplayer/data/__init__.py +1 -0
- multiplayer/data/cities.csv +103 -0
- multiplayer/data/countries.csv +152 -0
- multiplayer/data/egyptian_gods.csv +110 -0
- multiplayer/data/european_kings.csv +109 -0
- multiplayer/data/european_queens.csv +105 -0
- multiplayer/data/greek_gods.csv +122 -0
- multiplayer/data/planets_moons.csv +123 -0
- multiplayer/data/rivers.csv +103 -0
- multiplayer/data/roman_gods.csv +109 -0
- multiplayer/data/seas_oceans.csv +104 -0
- multiplayer/exceptions.py +39 -0
- multiplayer/game.py +275 -0
- multiplayer/language/__init__.py +23 -0
- multiplayer/language/language.py +445 -0
- multiplayer/py.typed +0 -0
- multiplayer/run_log_server.py +33 -0
- multiplayer/run_server.py +91 -0
- multiplayer/server.py +676 -0
- multiplayer/utils.py +215 -0
- multiplayer-0.11.0.dist-info/METADATA +284 -0
- multiplayer-0.11.0.dist-info/RECORD +31 -0
- multiplayer-0.11.0.dist-info/WHEEL +4 -0
- multiplayer-0.11.0.dist-info/entry_points.txt +4 -0
- multiplayer-0.11.0.dist-info/licenses/LICENSE.md +23 -0
multiplayer/client.py
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the client-side implementation for networked multiplayer games.
|
|
3
|
+
"""
|
|
4
|
+
import socket
|
|
5
|
+
import json
|
|
6
|
+
import struct
|
|
7
|
+
import time
|
|
8
|
+
import ssl
|
|
9
|
+
import logging
|
|
10
|
+
from logging.handlers import SocketHandler
|
|
11
|
+
from .game import Player, Observer
|
|
12
|
+
from . import exceptions
|
|
13
|
+
|
|
14
|
+
# Constants for network discovery
|
|
15
|
+
MULTICAST_GROUP = '224.1.1.1'
|
|
16
|
+
DISCOVERY_PORT = 5007
|
|
17
|
+
DISCOVERY_MESSAGE = b'multiplayer_game_discovery_request'
|
|
18
|
+
RESPONSE_MESSAGE_FORMAT = b'!15sH' # 15-char IP, unsigned short port
|
|
19
|
+
|
|
20
|
+
class GameClient:
|
|
21
|
+
"""
|
|
22
|
+
A client for connecting to a GameServer.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, host='127.0.0.1', port=65432, password=None, use_tls=False):
|
|
25
|
+
self.host = host
|
|
26
|
+
self.port = port
|
|
27
|
+
self.password = password
|
|
28
|
+
self.use_tls = use_tls
|
|
29
|
+
self._logger = logging.getLogger("GameClient")
|
|
30
|
+
self._logger.setLevel(logging.INFO)
|
|
31
|
+
self._logger.propagate = True # Ensure it bubbles up to root by default
|
|
32
|
+
# Check if root logger has a SocketHandler (configured by setup_logging in scripts)
|
|
33
|
+
# but better yet, let's look for any SocketHandler in the hierarchy
|
|
34
|
+
self._check_external_logging()
|
|
35
|
+
|
|
36
|
+
def _check_external_logging(self):
|
|
37
|
+
"""Checks if a SocketHandler is already configured in the logging hierarchy."""
|
|
38
|
+
curr = self._logger
|
|
39
|
+
while curr:
|
|
40
|
+
for h in curr.handlers:
|
|
41
|
+
if isinstance(h, SocketHandler):
|
|
42
|
+
return True
|
|
43
|
+
if not curr.propagate:
|
|
44
|
+
break
|
|
45
|
+
curr = curr.parent
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def configure_logging(self, host, port, name=None):
|
|
49
|
+
"""
|
|
50
|
+
Configures the client to send logs to a logging server.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
host (str): The host of the logging server.
|
|
54
|
+
port (int): The port of the logging server.
|
|
55
|
+
name (str, optional): A custom name for the logger.
|
|
56
|
+
"""
|
|
57
|
+
if name:
|
|
58
|
+
self._logger = logging.getLogger(name)
|
|
59
|
+
self._logger.setLevel(logging.INFO)
|
|
60
|
+
# Ensure propagation is True so it reaches root logger if configured there
|
|
61
|
+
self._logger.propagate = True
|
|
62
|
+
|
|
63
|
+
# Check if already connected to this host/port via hierarchy
|
|
64
|
+
if self._check_external_logging():
|
|
65
|
+
self._logger.info(f"Using existing IPC logging configuration for {self._logger.name}")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Remove existing SocketHandlers ONLY on this specific logger to avoid duplicates
|
|
69
|
+
# but keep others if they were there (though unlikely on this specific logger)
|
|
70
|
+
for h in self._logger.handlers[:]:
|
|
71
|
+
if isinstance(h, SocketHandler):
|
|
72
|
+
self._logger.removeHandler(h)
|
|
73
|
+
|
|
74
|
+
handler = SocketHandler(host, port)
|
|
75
|
+
self._logger.addHandler(handler)
|
|
76
|
+
self._logger.info(f"Logging configured for {self._logger.name} to {host}:{port}")
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def discover_servers(timeout=2):
|
|
80
|
+
"""
|
|
81
|
+
Discovers game servers on the local network using UDP multicast.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
timeout (int): The number of seconds to listen for responses.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A list of (host, port) tuples for discovered servers.
|
|
88
|
+
"""
|
|
89
|
+
servers = []
|
|
90
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) as sock:
|
|
91
|
+
sock.settimeout(timeout)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
sock.sendto(DISCOVERY_MESSAGE, (MULTICAST_GROUP, DISCOVERY_PORT))
|
|
95
|
+
except OSError:
|
|
96
|
+
# On some systems (like MacOS in CI), multicast might not be available
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
end_time = time.time() + timeout
|
|
100
|
+
while time.time() < end_time:
|
|
101
|
+
try:
|
|
102
|
+
data, _ = sock.recvfrom(1024)
|
|
103
|
+
ip_bytes, port = struct.unpack(RESPONSE_MESSAGE_FORMAT, data)
|
|
104
|
+
host = ip_bytes.decode('utf-8').strip('\x00')
|
|
105
|
+
servers.append((host, port))
|
|
106
|
+
except socket.timeout:
|
|
107
|
+
break
|
|
108
|
+
except Exception:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
return list(set(servers))
|
|
112
|
+
|
|
113
|
+
def _send_command(self, action, params=None, timeout=5):
|
|
114
|
+
"""Sends a command to the server and returns the response."""
|
|
115
|
+
self._logger.debug(f"Sending command {action} with params {params}")
|
|
116
|
+
try:
|
|
117
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
118
|
+
sock.settimeout(timeout)
|
|
119
|
+
|
|
120
|
+
conn = None
|
|
121
|
+
if self.use_tls:
|
|
122
|
+
context = ssl.create_default_context()
|
|
123
|
+
context.check_hostname = False
|
|
124
|
+
context.verify_mode = ssl.CERT_NONE # Accept self-signed cert
|
|
125
|
+
conn = context.wrap_socket(sock, server_hostname=self.host)
|
|
126
|
+
else:
|
|
127
|
+
conn = sock
|
|
128
|
+
|
|
129
|
+
with conn:
|
|
130
|
+
conn.connect((self.host, self.port))
|
|
131
|
+
command = {
|
|
132
|
+
'action': action,
|
|
133
|
+
'params': params or {},
|
|
134
|
+
'password': self.password,
|
|
135
|
+
}
|
|
136
|
+
conn.sendall(json.dumps(command).encode('utf-8'))
|
|
137
|
+
|
|
138
|
+
response_data = conn.recv(1024)
|
|
139
|
+
if not response_data:
|
|
140
|
+
raise exceptions.ConnectionError("Server closed the connection without a response (possible TLS mismatch).")
|
|
141
|
+
|
|
142
|
+
response = json.loads(response_data.decode('utf-8'))
|
|
143
|
+
|
|
144
|
+
if response.get('status') == 'error':
|
|
145
|
+
self._handle_error(response)
|
|
146
|
+
|
|
147
|
+
# If there's data, return it; otherwise return the response itself
|
|
148
|
+
# to allow checking 'status' or other fields for commands without 'data'.
|
|
149
|
+
if 'data' in response:
|
|
150
|
+
return response.get('data')
|
|
151
|
+
return response
|
|
152
|
+
except (socket.error, ssl.SSLError) as e:
|
|
153
|
+
raise exceptions.ConnectionError(f"Failed to connect to server: {e}")
|
|
154
|
+
except json.JSONDecodeError:
|
|
155
|
+
raise exceptions.ConnectionError("Failed to decode server response (possible TLS mismatch).")
|
|
156
|
+
|
|
157
|
+
def _handle_error(self, response):
|
|
158
|
+
"""Raises the appropriate client-side exception based on the server's response."""
|
|
159
|
+
error_type = response.get('type', 'ServerError')
|
|
160
|
+
message = response.get('message', 'An unknown error occurred.')
|
|
161
|
+
|
|
162
|
+
exception_class = getattr(exceptions, error_type, exceptions.ServerError)
|
|
163
|
+
raise exception_class(message)
|
|
164
|
+
|
|
165
|
+
def create_game(self, group_id=None, **game_options):
|
|
166
|
+
"""Requests the server to create a new game and returns a proxy to it."""
|
|
167
|
+
params = game_options.copy()
|
|
168
|
+
if group_id:
|
|
169
|
+
params['group_id'] = group_id
|
|
170
|
+
data = self._send_command('create_game', params)
|
|
171
|
+
remote_game = RemoteGame(data['game_id'], self.host, self.port, self.password, self.use_tls)
|
|
172
|
+
|
|
173
|
+
# Propagate logging configuration if any
|
|
174
|
+
for h in self._logger.handlers:
|
|
175
|
+
if isinstance(h, SocketHandler):
|
|
176
|
+
remote_game.configure_logging(h.host, h.port)
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
return remote_game
|
|
180
|
+
|
|
181
|
+
def list_games(self):
|
|
182
|
+
"""Retrieves a dictionary of active games as RemoteGame objects, indexed by ID."""
|
|
183
|
+
games_data = self._send_command('list_games')
|
|
184
|
+
|
|
185
|
+
remote_games = {}
|
|
186
|
+
for gid in games_data:
|
|
187
|
+
remote_games[gid] = RemoteGame(gid, self.host, self.port, self.password, self.use_tls)
|
|
188
|
+
|
|
189
|
+
# Propagate logging configuration if any
|
|
190
|
+
for h in self._logger.handlers:
|
|
191
|
+
if isinstance(h, SocketHandler):
|
|
192
|
+
remote_games[gid].configure_logging(h.host, h.port)
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
return remote_games
|
|
196
|
+
|
|
197
|
+
def create_group(self, name, admin_password=None, **attributes):
|
|
198
|
+
"""Requests the server to create a new game group and returns a proxy to it."""
|
|
199
|
+
data = self._send_command('create_group', {'name': name, 'admin_password': admin_password, 'attributes': attributes})
|
|
200
|
+
remote_group = RemoteGroup(data['group_id'], self.host, self.port, self.password, self.use_tls)
|
|
201
|
+
|
|
202
|
+
# Propagate logging configuration if any
|
|
203
|
+
for h in self._logger.handlers:
|
|
204
|
+
if isinstance(h, SocketHandler):
|
|
205
|
+
remote_group.configure_logging(h.host, h.port)
|
|
206
|
+
break
|
|
207
|
+
|
|
208
|
+
return remote_group
|
|
209
|
+
|
|
210
|
+
def list_groups(self):
|
|
211
|
+
"""Retrieves a dictionary of game groups as RemoteGroup objects, indexed by ID."""
|
|
212
|
+
groups_data = self._send_command('list_groups')
|
|
213
|
+
|
|
214
|
+
remote_groups = {}
|
|
215
|
+
for gid in groups_data:
|
|
216
|
+
remote_groups[gid] = RemoteGroup(gid, self.host, self.port, self.password, self.use_tls)
|
|
217
|
+
|
|
218
|
+
# Propagate logging configuration if any
|
|
219
|
+
for h in self._logger.handlers:
|
|
220
|
+
if isinstance(h, SocketHandler):
|
|
221
|
+
remote_groups[gid].configure_logging(h.host, h.port)
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
return remote_groups
|
|
225
|
+
|
|
226
|
+
class ServerAdmin:
|
|
227
|
+
"""
|
|
228
|
+
A client class for administrators to connect to and manage a GameServer.
|
|
229
|
+
"""
|
|
230
|
+
def __init__(self, host='127.0.0.1', port=65432, admin_password=None, use_tls=False):
|
|
231
|
+
self.host = host
|
|
232
|
+
self.port = port
|
|
233
|
+
self.admin_password = admin_password
|
|
234
|
+
self.use_tls = use_tls
|
|
235
|
+
self._client = GameClient(host, port, admin_password, use_tls)
|
|
236
|
+
self._logger = logging.getLogger("ServerAdmin")
|
|
237
|
+
self._logger.setLevel(logging.INFO)
|
|
238
|
+
|
|
239
|
+
def configure_logging(self, host, port):
|
|
240
|
+
"""Configures the admin client to send logs to a logging server."""
|
|
241
|
+
self._client.configure_logging(host, port, "ServerAdmin")
|
|
242
|
+
self._logger = self._client._logger
|
|
243
|
+
|
|
244
|
+
def stop_server(self):
|
|
245
|
+
"""Requests the server to shut down."""
|
|
246
|
+
return self._client._send_command('stop_server')
|
|
247
|
+
|
|
248
|
+
def restart_server(self):
|
|
249
|
+
"""Requests the server to restart (clears all current games)."""
|
|
250
|
+
return self._client._send_command('restart_server')
|
|
251
|
+
|
|
252
|
+
def get_server_info(self):
|
|
253
|
+
"""Retrieves information about the server's status and active games."""
|
|
254
|
+
return self._client._send_command('get_server_info')
|
|
255
|
+
|
|
256
|
+
def list_games(self):
|
|
257
|
+
"""Retrieves a list of available games from the server."""
|
|
258
|
+
return self._client.list_games()
|
|
259
|
+
|
|
260
|
+
def kick_player(self, game_id, player_id):
|
|
261
|
+
"""Kicks a player from a specific game."""
|
|
262
|
+
return self._client._send_command('kick_player', {'game_id': game_id, 'player_id': player_id})
|
|
263
|
+
|
|
264
|
+
def kick_observer(self, game_id, observer_id):
|
|
265
|
+
"""Kicks an observer from a specific game."""
|
|
266
|
+
return self._client._send_command('kick_observer', {'game_id': game_id, 'observer_id': observer_id})
|
|
267
|
+
|
|
268
|
+
def list_all_players(self):
|
|
269
|
+
"""Lists all players currently connected to the server across all games."""
|
|
270
|
+
return self._client._send_command('list_all_players')
|
|
271
|
+
|
|
272
|
+
def set_logging_config(self, host, port):
|
|
273
|
+
"""Sets the logging server address and port."""
|
|
274
|
+
return self._client._send_command('set_logging_config', {'host': host, 'port': port})
|
|
275
|
+
|
|
276
|
+
def set_logging_enabled(self, enabled):
|
|
277
|
+
"""Enables or disables logging on the server."""
|
|
278
|
+
return self._client._send_command('set_logging_enabled', {'enabled': enabled})
|
|
279
|
+
|
|
280
|
+
def get_cert_expiration(self):
|
|
281
|
+
"""Returns the expiration date of the server's TLS certificate."""
|
|
282
|
+
response = self._client._send_command('get_cert_expiration')
|
|
283
|
+
return response.get('expiration')
|
|
284
|
+
|
|
285
|
+
def set_server_password(self, new_password):
|
|
286
|
+
"""Sets a new password for the server."""
|
|
287
|
+
return self._client._send_command('set_server_password', {'new_password': new_password})
|
|
288
|
+
|
|
289
|
+
def set_admin_password(self, new_password):
|
|
290
|
+
"""Sets a new administrator password for the server."""
|
|
291
|
+
result = self._client._send_command('set_admin_password', {'new_password': new_password})
|
|
292
|
+
if result.get('status') == 'success':
|
|
293
|
+
self.admin_password = new_password
|
|
294
|
+
self._client.password = new_password
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
def create_group(self, name, admin_password=None, **attributes):
|
|
298
|
+
"""Creates a new game group on the server."""
|
|
299
|
+
return self._client.create_group(name, admin_password, **attributes)
|
|
300
|
+
|
|
301
|
+
def remove_group(self, group_id):
|
|
302
|
+
"""Removes a game group from the server by its ID."""
|
|
303
|
+
return self._client._send_command('remove_group', {'group_id': group_id})
|
|
304
|
+
|
|
305
|
+
def list_groups(self):
|
|
306
|
+
"""Retrieves a list of all game groups on the server as RemoteGroup objects."""
|
|
307
|
+
return self._client.list_groups()
|
|
308
|
+
|
|
309
|
+
class GroupAdmin:
|
|
310
|
+
"""
|
|
311
|
+
A client class for group administrators to manage games within a specific GameGroup.
|
|
312
|
+
"""
|
|
313
|
+
def __init__(self, group_id, host='127.0.0.1', port=65432, group_admin_password=None, use_tls=False):
|
|
314
|
+
self.group_id = group_id
|
|
315
|
+
self.host = host
|
|
316
|
+
self.port = port
|
|
317
|
+
self.group_admin_password = group_admin_password
|
|
318
|
+
self.use_tls = use_tls
|
|
319
|
+
self._client = GameClient(host, port, group_admin_password, use_tls)
|
|
320
|
+
self._logger = logging.getLogger(f"GroupAdmin.{group_id}")
|
|
321
|
+
self._logger.setLevel(logging.INFO)
|
|
322
|
+
|
|
323
|
+
def configure_logging(self, host, port):
|
|
324
|
+
"""Configures the group admin client to send logs to a logging server."""
|
|
325
|
+
self._client.configure_logging(host, port, f"GroupAdmin.{self.group_id}")
|
|
326
|
+
self._logger = self._client._logger
|
|
327
|
+
|
|
328
|
+
def list_games(self):
|
|
329
|
+
"""Retrieves a dictionary of games belonging to this group as RemoteGame objects, indexed by ID."""
|
|
330
|
+
remote_group = RemoteGroup(self.group_id, self.host, self.port, self.group_admin_password, self.use_tls)
|
|
331
|
+
|
|
332
|
+
# Propagate logging configuration if any
|
|
333
|
+
for h in self._logger.handlers:
|
|
334
|
+
if isinstance(h, SocketHandler):
|
|
335
|
+
remote_group.configure_logging(h.host, h.port)
|
|
336
|
+
break
|
|
337
|
+
|
|
338
|
+
return remote_group.list_games()
|
|
339
|
+
|
|
340
|
+
def kick_player(self, game_id, player_id):
|
|
341
|
+
"""Kicks a player from a specific game in the group."""
|
|
342
|
+
return self._client._send_command('kick_player', {
|
|
343
|
+
'game_id': game_id,
|
|
344
|
+
'player_id': player_id,
|
|
345
|
+
'group_id': self.group_id
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
def kick_observer(self, game_id, observer_id):
|
|
349
|
+
"""Kicks an observer from a specific game in the group."""
|
|
350
|
+
return self._client._send_command('kick_observer', {
|
|
351
|
+
'game_id': game_id,
|
|
352
|
+
'observer_id': observer_id,
|
|
353
|
+
'group_id': self.group_id
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
def set_group_admin_password(self, new_password):
|
|
357
|
+
"""Sets a new administrator password for this group."""
|
|
358
|
+
result = self._client._send_command('set_group_admin_password', {
|
|
359
|
+
'group_id': self.group_id,
|
|
360
|
+
'new_password': new_password
|
|
361
|
+
})
|
|
362
|
+
if result.get('status') == 'success':
|
|
363
|
+
self.group_admin_password = new_password
|
|
364
|
+
self._client.password = new_password
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
class RemoteGame:
|
|
368
|
+
"""
|
|
369
|
+
A proxy for a Game object on a remote server.
|
|
370
|
+
"""
|
|
371
|
+
def __init__(self, game_id, host='127.0.0.1', port=65432, password=None, use_tls=False):
|
|
372
|
+
self.game_id = game_id
|
|
373
|
+
self.host = host
|
|
374
|
+
self.port = port
|
|
375
|
+
self._client = GameClient(host, port, password, use_tls)
|
|
376
|
+
self._logger = logging.getLogger("RemoteGame")
|
|
377
|
+
self._logger.setLevel(logging.INFO)
|
|
378
|
+
self._logger.propagate = True
|
|
379
|
+
# RemoteGame should ideally use the same logger name if possible
|
|
380
|
+
# but for now we just ensure it propagates to the same destination.
|
|
381
|
+
|
|
382
|
+
def configure_logging(self, host, port, name=None):
|
|
383
|
+
"""Configures the remote game proxy to send logs to a logging server."""
|
|
384
|
+
if name is None:
|
|
385
|
+
name = f"RemoteGame.{self.game_id[:8]}"
|
|
386
|
+
self._client.configure_logging(host, port, name)
|
|
387
|
+
self._logger = self._client._logger
|
|
388
|
+
|
|
389
|
+
def _send_command(self, action, params=None):
|
|
390
|
+
"""Sends a command to the server for a specific game and returns the response."""
|
|
391
|
+
full_params = {'game_id': self.game_id}
|
|
392
|
+
if params:
|
|
393
|
+
full_params.update(params)
|
|
394
|
+
return self._client._send_command(action, full_params)
|
|
395
|
+
|
|
396
|
+
def add_player(self, player, password=None):
|
|
397
|
+
"""
|
|
398
|
+
Adds a player to the remote game.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
player (Player): The player to add.
|
|
402
|
+
password (str, optional): The password for this specific game.
|
|
403
|
+
"""
|
|
404
|
+
self._logger.info(f"Adding player {player.name} to game {self.game_id}")
|
|
405
|
+
params = {
|
|
406
|
+
'player': {'name': player.name, 'attributes': player.attributes},
|
|
407
|
+
'game_password': password,
|
|
408
|
+
}
|
|
409
|
+
self._send_command('add_player', params)
|
|
410
|
+
|
|
411
|
+
def add_observer(self, observer, password=None):
|
|
412
|
+
"""
|
|
413
|
+
Adds an observer to the remote game.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
observer (Observer): The observer to add.
|
|
417
|
+
password (str, optional): The password for observers of this game.
|
|
418
|
+
"""
|
|
419
|
+
params = {
|
|
420
|
+
'observer': {'name': observer.name, 'attributes': observer.attributes},
|
|
421
|
+
'observer_password': password,
|
|
422
|
+
}
|
|
423
|
+
self._send_command('add_observer', params)
|
|
424
|
+
|
|
425
|
+
def start(self):
|
|
426
|
+
"""Starts the remote game."""
|
|
427
|
+
self._logger.info(f"Starting game {self.game_id}")
|
|
428
|
+
self._send_command('start')
|
|
429
|
+
|
|
430
|
+
def pause(self):
|
|
431
|
+
"""Pauses the remote game."""
|
|
432
|
+
self._send_command('pause')
|
|
433
|
+
|
|
434
|
+
def resume(self):
|
|
435
|
+
"""Resumes the remote game."""
|
|
436
|
+
self._send_command('resume')
|
|
437
|
+
|
|
438
|
+
def stop(self):
|
|
439
|
+
"""Stops the remote game."""
|
|
440
|
+
self._send_command('stop')
|
|
441
|
+
|
|
442
|
+
def next_turn(self):
|
|
443
|
+
"""Advances to the next turn in the remote game."""
|
|
444
|
+
self._logger.debug(f"Advancing turn in game {self.game_id}")
|
|
445
|
+
self._send_command('next_turn')
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def current_player(self):
|
|
449
|
+
"""Gets the current player from the remote game."""
|
|
450
|
+
data = self._send_command('get_current_player')
|
|
451
|
+
if data:
|
|
452
|
+
return Player(data['name'], **data['attributes'])
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
@property
|
|
456
|
+
def state(self):
|
|
457
|
+
"""Gets the state of the remote game."""
|
|
458
|
+
return self._send_command('get_game_state')
|
|
459
|
+
|
|
460
|
+
@property
|
|
461
|
+
def players(self):
|
|
462
|
+
"""Gets the list of players in the remote game."""
|
|
463
|
+
data = self._send_command('get_players')
|
|
464
|
+
return [Player(p['name'], id=p['id'], **p['attributes']) for p in data]
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def observers(self):
|
|
468
|
+
"""Gets the list of observers in the remote game."""
|
|
469
|
+
data = self._send_command('get_observers')
|
|
470
|
+
return [Observer(o['name'], id=o['id'], **o['attributes']) for o in data]
|
|
471
|
+
|
|
472
|
+
def set_state(self, state):
|
|
473
|
+
"""Sets the state of the remote game."""
|
|
474
|
+
return self._send_command('set_game_state', {'state': state})
|
|
475
|
+
|
|
476
|
+
class RemoteGroup:
|
|
477
|
+
"""
|
|
478
|
+
A proxy for a GameGroup object on a remote server.
|
|
479
|
+
"""
|
|
480
|
+
def __init__(self, group_id, host='127.0.0.1', port=65432, password=None, use_tls=False):
|
|
481
|
+
self.group_id = group_id
|
|
482
|
+
self.host = host
|
|
483
|
+
self.port = port
|
|
484
|
+
self._client = GameClient(host, port, password, use_tls)
|
|
485
|
+
self._logger = logging.getLogger(f"RemoteGroup.{group_id}")
|
|
486
|
+
self._logger.setLevel(logging.INFO)
|
|
487
|
+
self._logger.propagate = True
|
|
488
|
+
|
|
489
|
+
def configure_logging(self, host, port, name=None):
|
|
490
|
+
"""Configures the remote group proxy to send logs to a logging server."""
|
|
491
|
+
if name is None:
|
|
492
|
+
name = f"RemoteGroup.{self.group_id[:8]}"
|
|
493
|
+
self._client.configure_logging(host, port, name)
|
|
494
|
+
self._logger = self._client._logger
|
|
495
|
+
|
|
496
|
+
def _send_command(self, action, params=None):
|
|
497
|
+
"""Sends a command to the server for a specific group and returns the response."""
|
|
498
|
+
full_params = {'group_id': self.group_id}
|
|
499
|
+
if params:
|
|
500
|
+
full_params.update(params)
|
|
501
|
+
return self._client._send_command(action, full_params)
|
|
502
|
+
|
|
503
|
+
def create_game(self, **game_options):
|
|
504
|
+
"""Creates a new game within this group."""
|
|
505
|
+
return self._client.create_game(group_id=self.group_id, **game_options)
|
|
506
|
+
|
|
507
|
+
def list_games(self):
|
|
508
|
+
"""Lists all games in this group."""
|
|
509
|
+
games_data = self._client._send_command('list_group_games', {'group_id': self.group_id})
|
|
510
|
+
|
|
511
|
+
remote_games = {}
|
|
512
|
+
for gid in games_data:
|
|
513
|
+
remote_games[gid] = RemoteGame(gid, self.host, self.port, self._client.password, self._client.use_tls)
|
|
514
|
+
|
|
515
|
+
# Propagate logging configuration if any
|
|
516
|
+
for h in self._logger.handlers:
|
|
517
|
+
if isinstance(h, SocketHandler):
|
|
518
|
+
remote_games[gid].configure_logging(h.host, h.port)
|
|
519
|
+
break
|
|
520
|
+
|
|
521
|
+
return remote_games
|
|
522
|
+
|
|
523
|
+
@property
|
|
524
|
+
def name(self):
|
|
525
|
+
"""Gets the name of the group."""
|
|
526
|
+
return self._send_command('get_group_info').get('name')
|
|
527
|
+
|
|
528
|
+
@property
|
|
529
|
+
def attributes(self):
|
|
530
|
+
"""Gets the attributes of the group."""
|
|
531
|
+
return self._send_command('get_group_info').get('attributes', {})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Data resources for multiplayer package."""
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
city
|
|
2
|
+
Tokyo
|
|
3
|
+
Delhi
|
|
4
|
+
Shanghai
|
|
5
|
+
Sao Paulo
|
|
6
|
+
Mumbai
|
|
7
|
+
Mexico City
|
|
8
|
+
Beijing
|
|
9
|
+
Osaka
|
|
10
|
+
Cairo
|
|
11
|
+
New York
|
|
12
|
+
Dhaka
|
|
13
|
+
Karachi
|
|
14
|
+
Buenos Aires
|
|
15
|
+
Kolkata
|
|
16
|
+
Istanbul
|
|
17
|
+
Chongqing
|
|
18
|
+
Lagos
|
|
19
|
+
Manila
|
|
20
|
+
Rio de Janeiro
|
|
21
|
+
Tianjin
|
|
22
|
+
Kinshasa
|
|
23
|
+
Guangzhou
|
|
24
|
+
Los Angeles
|
|
25
|
+
Moscow
|
|
26
|
+
Shenzhen
|
|
27
|
+
Lahore
|
|
28
|
+
Bangalore
|
|
29
|
+
Paris
|
|
30
|
+
Bogota
|
|
31
|
+
Jakarta
|
|
32
|
+
Chennai
|
|
33
|
+
Lima
|
|
34
|
+
Bangkok
|
|
35
|
+
Seoul
|
|
36
|
+
Nagoya
|
|
37
|
+
Hyderabad
|
|
38
|
+
London
|
|
39
|
+
Tehran
|
|
40
|
+
Chicago
|
|
41
|
+
Chengdu
|
|
42
|
+
Nanjing
|
|
43
|
+
Wuhan
|
|
44
|
+
Ho Chi Minh City
|
|
45
|
+
Luanda
|
|
46
|
+
Ahmedabad
|
|
47
|
+
Kuala Lumpur
|
|
48
|
+
Xian
|
|
49
|
+
Hong Kong
|
|
50
|
+
Dongguan
|
|
51
|
+
Hangzhou
|
|
52
|
+
Foshan
|
|
53
|
+
Shenyang
|
|
54
|
+
Riyadh
|
|
55
|
+
Baghdad
|
|
56
|
+
Santiago
|
|
57
|
+
Surat
|
|
58
|
+
Madrid
|
|
59
|
+
Suzhou
|
|
60
|
+
Pune
|
|
61
|
+
Harbin
|
|
62
|
+
Houston
|
|
63
|
+
Dallas
|
|
64
|
+
Toronto
|
|
65
|
+
Dar es Salaam
|
|
66
|
+
Miami
|
|
67
|
+
Belo Horizonte
|
|
68
|
+
Singapore
|
|
69
|
+
Philadelphia
|
|
70
|
+
Atlanta
|
|
71
|
+
Fukuoka
|
|
72
|
+
Khartoum
|
|
73
|
+
Barcelona
|
|
74
|
+
Johannesburg
|
|
75
|
+
Saint Petersburg
|
|
76
|
+
Qingdao
|
|
77
|
+
Dalian
|
|
78
|
+
Washington
|
|
79
|
+
Yangon
|
|
80
|
+
Alexandria
|
|
81
|
+
Jinan
|
|
82
|
+
Guadalajara
|
|
83
|
+
Ankara
|
|
84
|
+
Melbourne
|
|
85
|
+
Abidjan
|
|
86
|
+
Sydney
|
|
87
|
+
Monterrey
|
|
88
|
+
Changsha
|
|
89
|
+
Brasilia
|
|
90
|
+
Cape Town
|
|
91
|
+
Urumqi
|
|
92
|
+
Jeddah
|
|
93
|
+
Recife
|
|
94
|
+
Hanoi
|
|
95
|
+
Medellin
|
|
96
|
+
Zhengzhou
|
|
97
|
+
Fortaleza
|
|
98
|
+
Prague
|
|
99
|
+
Nairobi
|
|
100
|
+
Rome
|
|
101
|
+
Casablanca
|
|
102
|
+
Kunming
|
|
103
|
+
Salvador
|