Skip to content

Commit e1c2700

Browse files
committed
Implementing an auto-join mode
1 parent c64c1f7 commit e1c2700

File tree

3 files changed

+97
-18
lines changed

3 files changed

+97
-18
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ A lightweight server and framework for turn-based multiplayer games.
1717
- a framework that allows new games to be added easily
1818
- a uniform yet flexible API for all games
1919
- multiple parallel game sessions
20+
- you can join a specific session
21+
- or auto-join the next non-full session
2022
- an observer mode to watch another client play
2123

2224
### Designed for
@@ -51,11 +53,11 @@ Here is a simplified example of the API usage:
5153
```py
5254
from game_server_api import GameServerAPI
5355

54-
game = GameServerAPI(server='127.0.0.1', port=4711, game='TicTacToe',
55-
token='mygame', players=2)
56+
game = GameServerAPI(server='127.0.0.1', port=4711, game='TicTacToe', players=2,
57+
token='mygame') # pass 'auto' to auto-join a session
5658

57-
my_id = game.join() # starting/joining a game - each client is assigned an ID
58-
game.move(position=5) # performing a move - the function accepts keyword arguments (**kwargs)
59+
my_id = game.join() # start/join a session - each client is assigned an ID
60+
game.move(position=5) # perform a move - the function accepts keyword arguments (**kwargs)
5961
state = game.state() # returns a dictionary representing the game state,
6062
# including the ID of the current player(s)
6163
```

client/game_server_api.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,12 @@ def __init__(self, server, port, game, token, players=None, name=''):
5151
Parameters needed in order to connect to the server and to start or join
5252
a game session are passed to this constructor. Parameter game specifies
5353
the game to be started. It corresponds to the name of the game class on
54-
the server. To be able to join the game session, all participants need
55-
to agree on a token and pass it to the constructor. The token is used to
56-
identify the game session. It can be any string.
54+
the server. To be able to join a specific game session, all participants
55+
need to agree on a token and pass it to the constructor. The token is
56+
used to identify the game session. It can be any string. Alternatively,
57+
you can have the server automatically assign you to a session by passing
58+
the string 'auto' as the token. Refer to function join for more
59+
information.
5760
5861
The optional parameter players is required by function join in order to
5962
start a new game session with the specified number of players. If the
@@ -70,7 +73,7 @@ def __init__(self, server, port, game, token, players=None, name=''):
7073
server (str): server
7174
port (int): port number
7275
game (str): name of the game
73-
token (str): name of the game session
76+
token (str): name of the game session, 'auto' for automatic assignment
7477
players (int): total number of players (optional)
7578
name (str): player name (optional)
7679
@@ -81,7 +84,7 @@ def __init__(self, server, port, game, token, players=None, name=''):
8184
assert type(port) == int and 0 <= port <= 65535, self._error('port')
8285
assert type(game) == str and len(game) > 0, self._error('game')
8386
assert type(token) == str and len(token) > 0, self._error('token')
84-
assert players == None or type(players) == int and players > 0, self._error('players')
87+
assert players is None or type(players) == int and players > 0, self._error('players')
8588
assert type(name) == str, self._error('name')
8689

8790
# server:
@@ -110,8 +113,20 @@ def join(self):
110113
players must be passed to the constructor. If the argument is omitted,
111114
this function will only try to join an existing session but never start
112115
a new one. The argument is ignored when an existing session can be
113-
joined. If a session exists but is already fully occupied by players,
114-
it is terminated and a new session is started.
116+
joined.
117+
118+
There are two ways to start or join a game session:
119+
120+
- By providing a shared token to the constructor. All clients using the
121+
same token will join this specific game session. If such a session
122+
exists but is already fully occupied by players, it is terminated and
123+
a new session is started.
124+
- By passing the string 'auto' as the token. This causes the server to
125+
automatically assign you to an open session. If no session exists that
126+
can be joined, a new one is started. Existing sessions are never
127+
terminated. This method of starting and joining sessions does not
128+
interfere with the above method. To achieve this, the server creates
129+
unique tokens internally.
115130
116131
The game starts as soon as the required number of clients has joined the
117132
game. The function then returns the player ID. The server assigns IDs in
@@ -134,6 +149,7 @@ def join(self):
134149

135150
self._player_id = response['player_id']
136151
self._key = response['key']
152+
self._token = response['token']
137153
self._request_size_max = response['request_size_max']
138154

139155
return self._player_id
@@ -230,7 +246,8 @@ def observe(self):
230246
This function will return the player ID of the observed player.
231247
232248
This function can only be called, after the specified game session has
233-
already been started.
249+
already been started. The observer mode is not available for auto-join
250+
sessions.
234251
235252
Returns:
236253
int: ID of the observed player
@@ -241,6 +258,9 @@ def observe(self):
241258
if type(self._name) != str or len(self._name) == 0:
242259
raise GameServerError('a valid name must be passed to the constructor')
243260

261+
if self._token == 'auto':
262+
raise GameServerError('observer mode not available for auto-join sessions')
263+
244264
response, err, _ = self._send({
245265
'type':'observe',
246266
'game':self._game,

server/game_framework.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ def __init__(self):
4545
self._build_game_class_dict()
4646
self._start_clean_up()
4747
self._player_joins = threading.Event()
48+
self._auto_join_tokens = {} # game name -> auto-join token
49+
self._next_auto_join_token = 0
50+
self._AUTO = 'auto'
51+
self._lock = threading.Lock()
4852

4953
def _build_game_class_dict(self):
5054
"""
@@ -87,7 +91,9 @@ def _join(self, request):
8791
join. If this is the case, the request is passed to the function for
8892
joining a session. In all other cases, the function for starting a new
8993
game session is called. This implies that an already running session
90-
will be terminated.
94+
will be terminated. When a client makes use of the auto-join
95+
functionality, a unique token is generated. In this case, sessions are
96+
never terminated. A new session is created instead.
9197
9298
Parameters:
9399
request (dict): request containing game name and token
@@ -108,14 +114,23 @@ def _join(self, request):
108114
if game_name not in self._game_classes:
109115
return utility.framework_error('no such game')
110116

117+
if token.startswith(self._AUTO) and len(token) > len(self._AUTO):
118+
return utility.framework_error("token must not start with reserved prefix 'auto'")
119+
120+
# generate token for auto-joining clients:
121+
if token == self._AUTO:
122+
token = self._generate_auto_join_token(game_name, players)
123+
name = '' # no observer mode for auto-join sessions
124+
111125
# retrieve game session, if it exists:
112126
session, _ = self._retrieve_session(game_name, token)
113127

114128
# start or join a session:
115129
if session and not session.full():
116-
return self._join_session(session, name)
130+
return self._join_session(session, name, token)
117131
elif not session and not players:
118-
return utility.framework_error('no such game session')
132+
return utility.framework_error(
133+
'no such game session; provide the number of players to start one')
119134
elif session and session.full() and not players:
120135
return utility.framework_error('game session already full')
121136
else:
@@ -169,20 +184,22 @@ def _start_session(self, game_name, token, players, name):
169184

170185
# wait for others to join:
171186
self._await_game_start(session)
187+
self._remove_auto_join_token(game_name, token)
172188

173189
if not session.full(): # timeout reached
174190
if (game_name, token) in self._game_sessions:
175-
del self._game_sessions[(game_name, token)] # remove game session
191+
del self._game_sessions[(game_name, token)]
176192
return utility.framework_error('timeout while waiting for others to join')
177193

178194
log.info(f'Starting session {game_name}:{token}')
179195

180196
return self._return_data({
181197
'player_id':player_id,
182198
'key':key,
199+
'token':token,
183200
'request_size_max':config.request_size_max})
184201

185-
def _join_session(self, session, name):
202+
def _join_session(self, session, name, token):
186203
"""
187204
Joining a game session.
188205
@@ -199,6 +216,7 @@ def _join_session(self, session, name):
199216
Parameters:
200217
session (GameSession): game session
201218
name (str): player name, can be an empty string
219+
token (str): name of the game session
202220
203221
Returns:
204222
dict: containing the player's ID and key
@@ -217,6 +235,7 @@ def _join_session(self, session, name):
217235
return self._return_data({
218236
'player_id':player_id,
219237
'key':key,
238+
'token':token,
220239
'request_size_max':config.request_size_max})
221240

222241
def _move(self, request):
@@ -322,7 +341,8 @@ def _observe(self, request):
322341
needs to know the ID of that player. This function retrieves that ID
323342
based on the player's name. This only works, if the player has supplied
324343
a name when joining the game session. The observed player's key is sent
325-
to the client as well.
344+
to the client as well. The observer mode is not available for auto-join
345+
sessions.
326346
327347
Parameters:
328348
request (dict): request containing game name, token and player to be observed
@@ -338,6 +358,9 @@ def _observe(self, request):
338358
token = request['token']
339359
player_name = request['name']
340360

361+
if token == self._AUTO:
362+
return utility.framework_error('observer mode not available for auto-join sessions')
363+
341364
# retrieve game session:
342365
session, err = self._retrieve_session(game_name, token)
343366
if err: # no such game or game session
@@ -429,6 +452,40 @@ def _retrieve_session(self, game_name, token):
429452

430453
return self._game_sessions[(game_name, token)], None
431454

455+
def _generate_auto_join_token(self, game_name, players):
456+
"""
457+
Generate a unique token for an auto-join session.
458+
459+
Parameters:
460+
game_name (str): name of the game
461+
players (int): total number of players
462+
463+
Returns:
464+
str: a unique token for an auto-join session, None in case of an error
465+
"""
466+
with self._lock:
467+
if game_name not in self._auto_join_tokens:
468+
if not players: return None
469+
token = f'{self._AUTO}-{self._next_auto_join_token}'
470+
self._next_auto_join_token += 1
471+
self._auto_join_tokens[game_name] = token
472+
else:
473+
token = self._auto_join_tokens[game_name]
474+
475+
return token
476+
477+
def _remove_auto_join_token(self, game_name, token):
478+
"""
479+
This function is used to remove a token from the auto-join dictionary
480+
after the session has been started.
481+
482+
Parameters:
483+
game_name (str): name of the game
484+
token (str): name of the game session
485+
"""
486+
if token.startswith(self._AUTO):
487+
del self._auto_join_tokens[game_name]
488+
432489
def _return_data(self, data):
433490
"""
434491
Adds data to a dictionary to be sent back to the client. This function

0 commit comments

Comments
 (0)