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/utils.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides utility functions for the multiplayer package.
|
|
3
|
+
"""
|
|
4
|
+
import csv
|
|
5
|
+
import random
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# --- Built-in Categories ---
|
|
10
|
+
_BUILTIN_GAME_CATEGORIES = {
|
|
11
|
+
"cities": "data/cities.csv",
|
|
12
|
+
"countries": "data/countries.csv",
|
|
13
|
+
"rivers": "data/rivers.csv",
|
|
14
|
+
"seas_oceans": "data/seas_oceans.csv",
|
|
15
|
+
"planets_moons": "data/planets_moons.csv",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_BUILTIN_PLAYER_CATEGORIES = {
|
|
19
|
+
"roman_gods": "data/roman_gods.csv",
|
|
20
|
+
"greek_gods": "data/greek_gods.csv",
|
|
21
|
+
"egyptian_gods": "data/egyptian_gods.csv",
|
|
22
|
+
"european_kings": "data/european_kings.csv",
|
|
23
|
+
"european_queens": "data/european_queens.csv",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# --- Custom Categories (user-defined) ---
|
|
27
|
+
_CUSTOM_GAME_CATEGORIES = {}
|
|
28
|
+
_CUSTOM_PLAYER_CATEGORIES = {}
|
|
29
|
+
|
|
30
|
+
def register_name_category(category_name, data, category_type):
|
|
31
|
+
"""
|
|
32
|
+
Registers a new custom category for name suggestions.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
category_name (str): The name for the new category.
|
|
36
|
+
data (list or str or Path): A list of strings, or a path to a CSV/text file.
|
|
37
|
+
The file should have one name per line.
|
|
38
|
+
category_type (str): "game" or "player".
|
|
39
|
+
"""
|
|
40
|
+
if category_type == "game":
|
|
41
|
+
_CUSTOM_GAME_CATEGORIES[category_name] = data
|
|
42
|
+
elif category_type == "player":
|
|
43
|
+
_CUSTOM_PLAYER_CATEGORIES[category_name] = data
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("category_type must be 'game' or 'player'")
|
|
46
|
+
|
|
47
|
+
def unregister_name_category(category_name):
|
|
48
|
+
"""
|
|
49
|
+
Unregisters a custom category. Built-in categories cannot be removed.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
category_name (str): The name of the custom category to remove.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
bool: True if the category was found and removed, False otherwise.
|
|
56
|
+
"""
|
|
57
|
+
if category_name in _CUSTOM_GAME_CATEGORIES:
|
|
58
|
+
del _CUSTOM_GAME_CATEGORIES[category_name]
|
|
59
|
+
return True
|
|
60
|
+
if category_name in _CUSTOM_PLAYER_CATEGORIES:
|
|
61
|
+
del _CUSTOM_PLAYER_CATEGORIES[category_name]
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def get_available_categories(category_type="all"):
|
|
66
|
+
"""
|
|
67
|
+
Returns a list of available categories, including custom ones.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
category_type (str): "all", "game", or "player".
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A list of strings representing the available categories.
|
|
74
|
+
"""
|
|
75
|
+
if category_type == "game":
|
|
76
|
+
return list(_BUILTIN_GAME_CATEGORIES.keys()) + list(_CUSTOM_GAME_CATEGORIES.keys())
|
|
77
|
+
if category_type == "player":
|
|
78
|
+
return list(_BUILTIN_PLAYER_CATEGORIES.keys()) + list(_CUSTOM_PLAYER_CATEGORIES.keys())
|
|
79
|
+
return list({**_BUILTIN_GAME_CATEGORIES, **_BUILTIN_PLAYER_CATEGORIES, **_CUSTOM_GAME_CATEGORIES, **_CUSTOM_PLAYER_CATEGORIES}.keys())
|
|
80
|
+
|
|
81
|
+
def _get_names_from_source(source):
|
|
82
|
+
"""Internal helper to load names from a list, a file path, or a package resource."""
|
|
83
|
+
if isinstance(source, list):
|
|
84
|
+
return source
|
|
85
|
+
|
|
86
|
+
# Ensure source uses correct separators for the OS
|
|
87
|
+
source_path = Path(source)
|
|
88
|
+
|
|
89
|
+
# 1. Try relative to this file first (most reliable in both dev and production layouts)
|
|
90
|
+
try:
|
|
91
|
+
current_file_dir = Path(__file__).parent.resolve()
|
|
92
|
+
path = (current_file_dir / source_path).resolve()
|
|
93
|
+
if path.is_file():
|
|
94
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
95
|
+
if str(source).endswith('.csv'):
|
|
96
|
+
reader = csv.reader(f)
|
|
97
|
+
try:
|
|
98
|
+
next(reader) # Assume header
|
|
99
|
+
return [row[0] for row in reader if row]
|
|
100
|
+
except StopIteration:
|
|
101
|
+
return []
|
|
102
|
+
else:
|
|
103
|
+
return [line.strip() for line in f if line.strip()]
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
# 2. Try as an absolute or CWD-relative file path
|
|
108
|
+
try:
|
|
109
|
+
if source_path.is_file():
|
|
110
|
+
with open(source_path, 'r', encoding='utf-8') as f:
|
|
111
|
+
if str(source).endswith('.csv'):
|
|
112
|
+
reader = csv.reader(f)
|
|
113
|
+
try:
|
|
114
|
+
next(reader)
|
|
115
|
+
return [row[0] for row in reader if row]
|
|
116
|
+
except StopIteration:
|
|
117
|
+
return []
|
|
118
|
+
else:
|
|
119
|
+
return [line.strip() for line in f if line.strip()]
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
# 3. Fallback to package resource (standard PEP 302/modern way)
|
|
124
|
+
try:
|
|
125
|
+
# Try finding it in the multiplayer.data subpackage first
|
|
126
|
+
try:
|
|
127
|
+
# We explicitly import the subpackage to ensure it's loaded
|
|
128
|
+
import multiplayer.data
|
|
129
|
+
package_path = resources.files(multiplayer.data)
|
|
130
|
+
# If we are looking for 'data/cities.csv', and we are in 'multiplayer.data',
|
|
131
|
+
# we just need 'cities.csv'
|
|
132
|
+
if 'data' in source_path.parts:
|
|
133
|
+
file_name = source_path.name
|
|
134
|
+
resource_path = package_path.joinpath(file_name)
|
|
135
|
+
else:
|
|
136
|
+
resource_path = package_path.joinpath(*source_path.parts)
|
|
137
|
+
except (ImportError, ModuleNotFoundError, ValueError):
|
|
138
|
+
# Fallback to main package
|
|
139
|
+
import multiplayer
|
|
140
|
+
package_path = resources.files(multiplayer)
|
|
141
|
+
resource_path = package_path.joinpath(*source_path.parts)
|
|
142
|
+
|
|
143
|
+
if resource_path.is_file():
|
|
144
|
+
with resource_path.open('r', encoding='utf-8') as f:
|
|
145
|
+
if str(source).endswith('.csv'):
|
|
146
|
+
reader = csv.reader(f)
|
|
147
|
+
try:
|
|
148
|
+
next(reader) # Assume header
|
|
149
|
+
return [row[0] for row in reader if row]
|
|
150
|
+
except StopIteration:
|
|
151
|
+
return []
|
|
152
|
+
else:
|
|
153
|
+
return [line.strip() for line in f if line.strip()]
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def _suggest_from_category(category, valid_builtin_cats, valid_custom_cats):
|
|
160
|
+
"""Internal helper to suggest a name from a specific category."""
|
|
161
|
+
if category in valid_custom_cats:
|
|
162
|
+
source = valid_custom_cats[category]
|
|
163
|
+
elif category in valid_builtin_cats:
|
|
164
|
+
source = valid_builtin_cats[category]
|
|
165
|
+
else:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
names = _get_names_from_source(source)
|
|
169
|
+
if not names:
|
|
170
|
+
return None
|
|
171
|
+
return random.choice(names)
|
|
172
|
+
|
|
173
|
+
def suggest_game_name(category=None):
|
|
174
|
+
"""
|
|
175
|
+
Suggests a random game name.
|
|
176
|
+
|
|
177
|
+
If a category is provided, a name is chosen from that category.
|
|
178
|
+
If no category is provided, a random game-related category is chosen first.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
category (str, optional): A category from get_available_categories("game").
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
A string containing a random name, or None on failure.
|
|
185
|
+
"""
|
|
186
|
+
if category:
|
|
187
|
+
return _suggest_from_category(category, _BUILTIN_GAME_CATEGORIES, _CUSTOM_GAME_CATEGORIES)
|
|
188
|
+
|
|
189
|
+
all_game_cats = {**_BUILTIN_GAME_CATEGORIES, **_CUSTOM_GAME_CATEGORIES}
|
|
190
|
+
if not all_game_cats:
|
|
191
|
+
return None
|
|
192
|
+
random_category = random.choice(list(all_game_cats.keys()))
|
|
193
|
+
return _suggest_from_category(random_category, _BUILTIN_GAME_CATEGORIES, _CUSTOM_GAME_CATEGORIES)
|
|
194
|
+
|
|
195
|
+
def suggest_player_name(category=None):
|
|
196
|
+
"""
|
|
197
|
+
Suggests a random player name.
|
|
198
|
+
|
|
199
|
+
If a category is provided, a name is chosen from that category.
|
|
200
|
+
If no category is provided, a random player-related category is chosen first.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
category (str, optional): A category from get_available_categories("player").
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
A string containing a random name, or None on failure.
|
|
207
|
+
"""
|
|
208
|
+
if category:
|
|
209
|
+
return _suggest_from_category(category, _BUILTIN_PLAYER_CATEGORIES, _CUSTOM_PLAYER_CATEGORIES)
|
|
210
|
+
|
|
211
|
+
all_player_cats = {**_BUILTIN_PLAYER_CATEGORIES, **_CUSTOM_PLAYER_CATEGORIES}
|
|
212
|
+
if not all_player_cats:
|
|
213
|
+
return None
|
|
214
|
+
random_category = random.choice(list(all_player_cats.keys()))
|
|
215
|
+
return _suggest_from_category(random_category, _BUILTIN_PLAYER_CATEGORIES, _CUSTOM_PLAYER_CATEGORIES)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: multiplayer
|
|
3
|
+
Version: 0.11.0
|
|
4
|
+
Summary: Library that allows you to manage multiple players, locally, on a network, or on the Internet.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE.md
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Topic :: Games/Entertainment
|
|
14
|
+
Requires-Dist: colorlog
|
|
15
|
+
Requires-Dist: cryptography
|
|
16
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
17
|
+
Requires-Dist: requests ; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff ; extra == 'dev'
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
**English** | [Español](translation/README.es.md) | [Français](translation/README.fr.md)
|
|
24
|
+
|
|
25
|
+
# Multiplayer Game Manager
|
|
26
|
+
|
|
27
|
+
> **A Note on this Project's Origin**
|
|
28
|
+
>
|
|
29
|
+
> This project is primarily the result of a series of experiments using Gemini Code Assist for code generation and error handling. Rather than using it on academic examples, it seemed more interesting to apply it to a project that could meet a real practical need.
|
|
30
|
+
>
|
|
31
|
+
> This, therefore, is the reason for `multiplayer`'s existence: you can dissect the code to see how Gemini (with my guidance) went about building it, or you can ignore all that and just use this library for your own needs!
|
|
32
|
+
|
|
33
|
+
This Python module provides a simple and flexible framework for managing multiplayer games, both locally and over a network.
|
|
34
|
+
|
|
35
|
+
For a detailed technical description of all classes and functions, see the [API Reference](REFERENCE.md).
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
* **Local & Networked:** Use in a single process or in a client-server architecture.
|
|
40
|
+
* **Combined Game State:** A flexible system for synchronizing both the core game status (e.g., `in_progress`) and any custom game data.
|
|
41
|
+
* **Observer Support:** Ability to add observers who can view the game state without participating as players.
|
|
42
|
+
* **Administrator Role:** New `ServerAdmin` class to manage the server, kick players/observers, and monitor server status.
|
|
43
|
+
* **Game Grouping:** Organize several game sessions within the same server using the `GameGroup` class.
|
|
44
|
+
* **Multi-Layered Security:** Supports server passwords, admin passwords, and per-game passwords, with optional TLS v1.3 encryption. Passwords can be updated dynamically by administrators.
|
|
45
|
+
* **Automatic Server Discovery:** Clients can automatically find running servers on the local network.
|
|
46
|
+
* **Extensible Name Suggestions:** Includes a utility function to suggest creative names for games and players.
|
|
47
|
+
* **Multiple Games:** The server can manage multiple game sessions simultaneously, and the game list is now filtered to hide finished games.
|
|
48
|
+
* **Robust Error Handling:** A clear set of custom exceptions for both game logic and network issues.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
You can install it in two ways:
|
|
53
|
+
|
|
54
|
+
### 1. From PyPI
|
|
55
|
+
```sh
|
|
56
|
+
pip install multiplayer
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. From a Wheel file (GitHub)
|
|
60
|
+
Download the `.whl` file from the [Releases](https://github.com/devfred78/multiplayer/releases) page and run:
|
|
61
|
+
```sh
|
|
62
|
+
pip install multiplayer-0.11.0-py3-none-any.whl
|
|
63
|
+
```
|
|
64
|
+
*Replace `multiplayer-0.11.0-py3-none-any.whl` with the actual name of the downloaded file.*
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Game State Management
|
|
69
|
+
|
|
70
|
+
A key feature is the ability to manage your own game state alongside the core game status.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# On one client, set a custom state
|
|
74
|
+
game.set_state({
|
|
75
|
+
"board": [["X", "O", ""], ["", "X", ""], ["O", "", ""]],
|
|
76
|
+
"turn": "player2"
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
# On another client, retrieve the combined state
|
|
80
|
+
full_state = game.state
|
|
81
|
+
print(f"Game status: {full_state['status']}")
|
|
82
|
+
# > Game status: in_progress
|
|
83
|
+
|
|
84
|
+
print(f"Current turn: {full_state['custom']['turn']}")
|
|
85
|
+
# > Current turn: player2
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
### Game Grouping
|
|
90
|
+
|
|
91
|
+
You can group games together for better organization.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from multiplayer import Game, GameGroup
|
|
95
|
+
|
|
96
|
+
# Create a group
|
|
97
|
+
group = GameGroup("Tournament A", priority="high")
|
|
98
|
+
|
|
99
|
+
# Add games to the group
|
|
100
|
+
game1 = Game("Match 1")
|
|
101
|
+
game2 = Game("Match 2")
|
|
102
|
+
group.add_game(game1)
|
|
103
|
+
group.add_game(game2)
|
|
104
|
+
|
|
105
|
+
print(f"Game 1 ID: {game1.ID}")
|
|
106
|
+
# > Game 1 ID: 550e8400-e29b-41d4-a716-446655440000
|
|
107
|
+
|
|
108
|
+
print(f"Group '{group.name}' has {len(group.games)} games.")
|
|
109
|
+
# > Group 'Tournament A' has 2 games.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
### Full Test Environment
|
|
114
|
+
|
|
115
|
+
A script is available to launch a complete test environment with:
|
|
116
|
+
- An IPC log server (`IPClogging`) in a separate window.
|
|
117
|
+
- A game server.
|
|
118
|
+
- Multiple separate client instances (default is 2) simulating a game, each in its own terminal window.
|
|
119
|
+
|
|
120
|
+
To run it:
|
|
121
|
+
```bash
|
|
122
|
+
uv run python scripts/full_test_env.py
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
To specify the number of players:
|
|
126
|
+
```bash
|
|
127
|
+
uv run python scripts/full_test_env.py --players 3
|
|
128
|
+
```
|
|
129
|
+
This will open several Windows Terminal windows: one for the log server and one for each client instance, allowing you to see the real-time interactions and logs.
|
|
130
|
+
|
|
131
|
+
### Local Usage
|
|
132
|
+
|
|
133
|
+
You can use the `Game` class directly, including with a password for local validation.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from multiplayer import Game, Player, suggest_game_name
|
|
137
|
+
|
|
138
|
+
game = Game(name="My Awesome Game", password="local_game_pass")
|
|
139
|
+
game.add_player(Player("Alice"), password="local_game_pass")
|
|
140
|
+
game.start()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Networked Usage (Client-Server)
|
|
144
|
+
|
|
145
|
+
#### Server Setup
|
|
146
|
+
```python
|
|
147
|
+
from multiplayer import GameServer
|
|
148
|
+
|
|
149
|
+
# Start a secure server with a custom domain and self-signed certificate
|
|
150
|
+
server = GameServer(
|
|
151
|
+
host='0.0.0.0',
|
|
152
|
+
port=12345,
|
|
153
|
+
name="My Production Server",
|
|
154
|
+
password="my_server_password",
|
|
155
|
+
admin_password="my_admin_password",
|
|
156
|
+
use_tls=True,
|
|
157
|
+
tls_domain="example.com",
|
|
158
|
+
tls_self_signed=True
|
|
159
|
+
)
|
|
160
|
+
server.start()
|
|
161
|
+
|
|
162
|
+
# Or use existing certificate files
|
|
163
|
+
server = GameServer(
|
|
164
|
+
use_tls=True,
|
|
165
|
+
tls_cert="path/to/cert.pem",
|
|
166
|
+
tls_key="path/to/key.pem",
|
|
167
|
+
tls_self_signed=False
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Running with Docker
|
|
172
|
+
|
|
173
|
+
You can run the game server using Docker. To use your own TLS certificates, map a local directory containing `cert.pem` and `privkey.pem` to `/app/certs` in the container.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
docker run -d \
|
|
177
|
+
-p 65432:65432 \
|
|
178
|
+
-v /path/to/your/certs:/app/certs \
|
|
179
|
+
ghcr.io/yourusername/multiplayer-server:latest \
|
|
180
|
+
--name "My Docker Server" \
|
|
181
|
+
--use-tls --no-self-signed
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The server will automatically look for `cert.pem`, `RSA-cert.pem`, or `ECC-cert.pem` (and their corresponding keys) in the `/app/certs` directory.
|
|
185
|
+
|
|
186
|
+
#### Administrator Usage
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from multiplayer import ServerAdmin
|
|
190
|
+
|
|
191
|
+
# Connect as administrator
|
|
192
|
+
admin = ServerAdmin(
|
|
193
|
+
host='localhost',
|
|
194
|
+
port=12345,
|
|
195
|
+
admin_password="my_admin_password",
|
|
196
|
+
use_tls=True
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Manage the server
|
|
200
|
+
info = admin.get_server_info()
|
|
201
|
+
print(f"Active games: {info['games_count']}")
|
|
202
|
+
|
|
203
|
+
# Check certificate expiration
|
|
204
|
+
expiration = admin.get_cert_expiration()
|
|
205
|
+
print(f"Certificate expires on: {expiration}")
|
|
206
|
+
|
|
207
|
+
# Kick a player if necessary
|
|
208
|
+
# admin.kick_player(game_id, player_id)
|
|
209
|
+
|
|
210
|
+
# Stop the server remotely
|
|
211
|
+
# admin.stop_server()
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### Client Usage
|
|
215
|
+
```python
|
|
216
|
+
from multiplayer import GameClient, Player, suggest_game_name
|
|
217
|
+
|
|
218
|
+
# 1. Discover and connect to the server
|
|
219
|
+
servers = GameClient.discover_servers()
|
|
220
|
+
if not servers:
|
|
221
|
+
print("No servers found.")
|
|
222
|
+
else:
|
|
223
|
+
host, port = servers[0]
|
|
224
|
+
client = GameClient(
|
|
225
|
+
host=host,
|
|
226
|
+
port=port,
|
|
227
|
+
password="my_server_password",
|
|
228
|
+
use_tls=True
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# 2. Create a private game
|
|
232
|
+
private_game = client.create_game(
|
|
233
|
+
name=suggest_game_name(),
|
|
234
|
+
password="my_game_password"
|
|
235
|
+
)
|
|
236
|
+
print(f"Created game with ID: {private_game.ID}")
|
|
237
|
+
|
|
238
|
+
# 3. Create and use a Game Group
|
|
239
|
+
group = client.create_group("Tournament A")
|
|
240
|
+
game_in_group = group.create_game(name="Final Match")
|
|
241
|
+
print(f"Game in group '{group.name}' has ID: {game_in_group.ID}")
|
|
242
|
+
|
|
243
|
+
# 4. A player joins and sets the initial state
|
|
244
|
+
private_game.add_player(Player("Charlie"), password="my_game_password")
|
|
245
|
+
private_game.set_state({"score": 0})
|
|
246
|
+
private_game.start()
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Error Handling
|
|
250
|
+
|
|
251
|
+
The module provides a set of custom exceptions, including `AuthenticationError` for both server and game passwords.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from multiplayer import GameClient
|
|
255
|
+
from multiplayer.exceptions import ConnectionError, AuthenticationError
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
# ... connect to client ...
|
|
259
|
+
|
|
260
|
+
# Try to join a game with the wrong password
|
|
261
|
+
game.add_player(Player("Eve"), password="wrong_game_password")
|
|
262
|
+
|
|
263
|
+
except AuthenticationError as e:
|
|
264
|
+
print(f"Authentication failed as expected: {e}")
|
|
265
|
+
except ConnectionError as e:
|
|
266
|
+
print(f"A connection or discovery error occurred: {e}")
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Contributing
|
|
270
|
+
|
|
271
|
+
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more details on how to get started.
|
|
272
|
+
|
|
273
|
+
## Running Tests
|
|
274
|
+
|
|
275
|
+
To run the unit tests, you will need to have `pytest` installed.
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
pip install pytest
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Then, you can run the tests from the root of the project:
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
pytest
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
multiplayer/IPClogging/__init__.py,sha256=_0l2znu2eZsl7dYT6JCd23TQGwqMOfkTPLjtDOb-TwA,1590
|
|
2
|
+
multiplayer/IPClogging/echoing.py,sha256=pj8xGvvvxhRDE4dJXBip66vx5vyNWFYPmfee-fPc1cw,1153
|
|
3
|
+
multiplayer/IPClogging/server.py,sha256=E9STqw9QmkmQ0t4DnKKXK-verzwdY9IpAWXfcycczZ8,13753
|
|
4
|
+
multiplayer/IPClogging/test.py,sha256=SSgp6KQoZn6PnbLcCOiqlwAS_D70koSyMmfQWlcCxYQ,2335
|
|
5
|
+
multiplayer/__init__.py,sha256=DDXy3jtiqT378FI4fae9BU90L6BL_Q5tgLPeehdp3sI,1212
|
|
6
|
+
multiplayer/client.py,sha256=lzelylHYH0XbOhe0OwhonCnSdIqg5B-sVPhQ3fjY-qU,21428
|
|
7
|
+
multiplayer/data/__init__.py,sha256=IKVEo67OxQU1W-7JXrsuKCNA3Lc6wbcPbMYSlKsboPk,46
|
|
8
|
+
multiplayer/data/cities.csv,sha256=SQlwCxbofWxQN41NQUsybRsC5gAHvaDGkm7A7l9JlLQ,881
|
|
9
|
+
multiplayer/data/countries.csv,sha256=sB2MYdbNbKW2a6OaySeXNG4017dK6Pn5TQrKmt1s0V4,1351
|
|
10
|
+
multiplayer/data/egyptian_gods.csv,sha256=otF4bFk3P410W3nzXr7W-Byl6W5Bhmm2tVUpZRMK-Oo,696
|
|
11
|
+
multiplayer/data/european_kings.csv,sha256=i3fpfyK2KnPj8tzMO5DCB3r2XxCeKnFyXVmb6PqwQGs,1341
|
|
12
|
+
multiplayer/data/european_queens.csv,sha256=YxOfgU68HS3NktMu2ovDbmGwOmnGAjwUpctp5FHDuMM,1938
|
|
13
|
+
multiplayer/data/greek_gods.csv,sha256=D0yZG2TV8GpKVxMkcx-e8kP-vMvvbha9xRapx_ENbUo,917
|
|
14
|
+
multiplayer/data/planets_moons.csv,sha256=xBnp_DLsyoJX8BM48sKlQ_AR0CS0PJFVH14_B0GNaGs,909
|
|
15
|
+
multiplayer/data/rivers.csv,sha256=FK2l4Yt7RePcjQm48EfI_Ksxr4XhMKkyJVvSMYfz7LE,782
|
|
16
|
+
multiplayer/data/roman_gods.csv,sha256=KqzP5PcA68-Bj6hO7NefF6anb1ABzR5zvJ3MAXjJy88,819
|
|
17
|
+
multiplayer/data/seas_oceans.csv,sha256=VUzNRxEmjWePa7owC8GNnobOlbpEUUxEDG6KsJKZ3pA,1347
|
|
18
|
+
multiplayer/exceptions.py,sha256=mOYRbVRJU5W3wUORPPzsnbCI7xJ5DGDig1lUVhMkYmw,1052
|
|
19
|
+
multiplayer/game.py,sha256=Fup2BbTGrBvlUqZq8dW773HSUfYJ1G3Ak-6CXIphIOM,9020
|
|
20
|
+
multiplayer/language/__init__.py,sha256=uBWyC9rEkGtR5-_uGRqQWZ9ycH-NeO1L7JXzHJioKcg,869
|
|
21
|
+
multiplayer/language/language.py,sha256=5KcaDGAaGukMTYdw0JfuNuimpRmtWR0pBxkLG3A2mVo,21299
|
|
22
|
+
multiplayer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
multiplayer/run_log_server.py,sha256=FdQ_cTzxxEyeRYARGdxZHKEjD6cC9LmhMqNuacvb5Yw,915
|
|
24
|
+
multiplayer/run_server.py,sha256=l5y0tbnthpqUcQPCSKKVIcbEvBas_LdIm3GcwWJMf1g,3921
|
|
25
|
+
multiplayer/server.py,sha256=6b6T2-0TYGnxuoHEWmpB9GudAAxV7kfIYbhLcEh7JME,31217
|
|
26
|
+
multiplayer/utils.py,sha256=-kJ9qCx7BQ3GaTQlaoTbR6eqJP1_txYVuPRMP8v0r6A,7973
|
|
27
|
+
multiplayer-0.11.0.dist-info/licenses/LICENSE.md,sha256=kzBwgJsF0xF7Sn6fzdG1w10J1pUiaPZV_7qogwPxpd4,1161
|
|
28
|
+
multiplayer-0.11.0.dist-info/WHEEL,sha256=q5IF0q2xCp3ktUFRCVWsQLjl2ChNlWXBJtnI1LCGdJ8,80
|
|
29
|
+
multiplayer-0.11.0.dist-info/entry_points.txt,sha256=17_5t71OVCgGXdxI8bEfqjCFPUkn6PWcCF9CshY_cKg,125
|
|
30
|
+
multiplayer-0.11.0.dist-info/METADATA,sha256=92VkL11dX-CD85jr4FMHfs-sCCMjH0WRSCFHx4sKPo4,8861
|
|
31
|
+
multiplayer-0.11.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
**English** | [Español](translation/LICENSE.es.md) | [Français](translation/LICENSE.fr.md)
|
|
2
|
+
|
|
3
|
+
# MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026 devfred78
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify,merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT of OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|