Turn-based multiplayer games are a great opportunity for learning and prototyping: simple enough to implement quickly, but real enough to teach networking, game state, and API design.
I wrote a lightweight server and framework for turn-based multiplayer games that provides exactly that: a lightweight server + uniform API so you can run multiple parallel game sessions, auto-join the next available session, and add new games without touching the core API. It's built to be friendly for beginners (Python-only, standard library), but still flexible enough to support arbitrary game logic via keyword-argument moves and dict-based state.
Key bits:
- Framework for adding new games by deriving from an
AbstractGamebase class - Uniform client API for joining sessions, submitting moves, retrieving state, restarting
- Demo clients included (e.g. TicTacToe) and a template for new games
- Runs multiple sessions simultaneously; clients can join a specific session or auto-join
Repository: github.com/feberts/python-game-server
If you're teaching Python, building small multiplayer projects, or just want a clean starting point for turn-based game networking, I'd love feedback and contributions.
In this article, you'll learn how to implement clients and add new games to the server.
Implementing clients
This section demonstrates the usage of the API functions.
Starting and joining a game
To connect to the server and join a game session, you have to provide at least the following information to the constructor of the API class:
from game_server_api import GameServerAPI, GameServerError, IllegalMove
game = GameServerAPI(server='127.0.0.1', port=4711, game='TicTacToe')
You can then try to join a game session:
my_id = game.join()
The function blocks until enough players have joined the session. On success, the player ID is returned.
If no game session exists that can be joined, a GameServerError exception will be raised. This happens when all sessions are fully occupied or when no session exists at all. A new and empty session will not be created.
To start a new session, the number of players must be passed to the constructor.
game = GameServerAPI(server='127.0.0.1', port=4711, game='TicTacToe', players=2)
Now, calling join() will first try to join an existing session. If no session can be joined, a new one will be started.
If you want to play against your peers, you can agree on a session token:
game = GameServerAPI(server='127.0.0.1', port=4711,
game='TicTacToe', session='mygame',
players=2)
Every client using this token, will join this specific session. The number of players is still optional for joining a session, but required to start a new one.
If a session token is used and join() is called during a session, the current session will be terminated and a new one created. When no token is used (auto-join), sessions are never terminated calling join(). Instead, a new session is started.
Submitting moves
Function move() expects keyword arguments (**kwargs). Any number of keyword arguments can be passed. Refer to the documentation of a specific game to learn about the required arguments.
In the below example, an integer is passed as the value to the key position. This is expected by the tic-tac-toe implementation.
try:
game.move(position=7)
except IllegalMove as e:
print(e)
If the move is illegal, an IllegalMove exception is raised. This will happen if it is not the player's turn to perform a move or if the move itself is not valid.
Retrieving the state
The state is retrieved with function state(). It returns a dictionary:
state = game.state()
This function will block until the game state actually changes. Only then will the server respond with the updated state. To avoid deadlocks, the function never blocks in certain situations. This way, the game state is always available when needed.
The dictionary will always at least contain these two keys:
game_status = state['gameover']
current_player = state['current']
-
'current': a list of player IDs, indicating whose player's turn it is -
'gameover': a boolean value indicating whether the game has ended or is still active
Other key-value-pairs depend on the specific game.
Example
This is a simplified example using the API functions described above. The example shows a typical game loop.
from game_server_api import GameServerAPI, IllegalMove
game = GameServerAPI(server='127.0.0.1', port=4711, game='TicTacToe', players=2,
session='mygame') # pass 'auto' to auto-join a session (default)
my_id = game.join() # start/join a session - each client is assigned an ID
state = game.state() # returns a dictionary representing the game state
while not state['gameover']:
board = state['board']
# print game board here ...
if my_id in state['current']:
pos = int(input('Your turn: '))
try:
game.move(position=pos) # perform a move - the function accepts keyword arguments
except IllegalMove as e:
# something went wrong ...
else:
# opponent's turn ...
state = game.state() # to prevent polling, the function blocks until the state changes
Restarting games
Games can be restarted by calling function restart(). There is no need to rejoin the session, and all players will keep their IDs. The server ensures that all clients will receive the state of the previous game a last time before receiving the new game's state. This way they will not miss the end/outcome of the previous game.
Enabling TLS
TLS can be enforced by the server. If this is the case, all clients must enable TLS as well by calling function enable_tls(). Refer to the API reference for more information.
Adding new games
Adding a new game is easy. All you have to do is derive from a base class and override a handful of methods. The easiest way to add a game is to use the template (server/games/template.py), which is structured like a tutorial.
Here is a summary of the required steps:
- Create a new module in
server/games/. - Implement a class that is derived from
AbstractGame. - Override the base class's methods.
- Add the new class to the list of games (
server/games_list.py).
No modifications to the API are required when adding new games. It was designed to be compatible with any game. The function to submit a move accepts keyword arguments (**kwargs). These are sent to the server and passed to the game class as a dictionary. The game state is also sent back as a dictionary. This allows for a maximum of flexibility.
Top comments (0)