mudgym 0.3.0__tar.gz
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.
- mudgym-0.3.0/LICENSE +21 -0
- mudgym-0.3.0/NOTICE +13 -0
- mudgym-0.3.0/PKG-INFO +69 -0
- mudgym-0.3.0/README.md +35 -0
- mudgym-0.3.0/pyproject.toml +117 -0
- mudgym-0.3.0/pyproject.toml.orig +105 -0
- mudgym-0.3.0/src/mudgym/__init__.py +24 -0
- mudgym-0.3.0/src/mudgym/actions.py +11 -0
- mudgym-0.3.0/src/mudgym/connections/__init__.py +0 -0
- mudgym-0.3.0/src/mudgym/connections/config.py +17 -0
- mudgym-0.3.0/src/mudgym/connections/connection.py +150 -0
- mudgym-0.3.0/src/mudgym/connections/docker_exec.py +152 -0
- mudgym-0.3.0/src/mudgym/connections/docker_image.py +98 -0
- mudgym-0.3.0/src/mudgym/connections/docker_run.py +95 -0
- mudgym-0.3.0/src/mudgym/connections/persona.py +47 -0
- mudgym-0.3.0/src/mudgym/connections/prompts.py +298 -0
- mudgym-0.3.0/src/mudgym/connections/provider.py +250 -0
- mudgym-0.3.0/src/mudgym/connections/recording.py +218 -0
- mudgym-0.3.0/src/mudgym/connections/registry.py +81 -0
- mudgym-0.3.0/src/mudgym/connections/state_machine.py +533 -0
- mudgym-0.3.0/src/mudgym/connections/termination.py +47 -0
- mudgym-0.3.0/src/mudgym/connections/transitions.py +234 -0
- mudgym-0.3.0/src/mudgym/db/directions.py +38 -0
- mudgym-0.3.0/src/mudgym/db/index.py +122 -0
- mudgym-0.3.0/src/mudgym/db/levels.py +68 -0
- mudgym-0.3.0/src/mudgym/db/rooms.py +1156 -0
- mudgym-0.3.0/src/mudgym/db/weather.py +15 -0
- mudgym-0.3.0/src/mudgym/envs/__init__.py +0 -0
- mudgym-0.3.0/src/mudgym/envs/actions/__init__.py +0 -0
- mudgym-0.3.0/src/mudgym/envs/actions/discrete.py +53 -0
- mudgym-0.3.0/src/mudgym/envs/env.py +375 -0
- mudgym-0.3.0/src/mudgym/envs/factory.py +273 -0
- mudgym-0.3.0/src/mudgym/envs/fields/__init__.py +31 -0
- mudgym-0.3.0/src/mudgym/envs/fields/feinventory.py +75 -0
- mudgym-0.3.0/src/mudgym/envs/fields/fescore.py +140 -0
- mudgym-0.3.0/src/mudgym/envs/fields/fexits.py +92 -0
- mudgym-0.3.0/src/mudgym/envs/fields/field.py +126 -0
- mudgym-0.3.0/src/mudgym/envs/fields/mgcheats.py +113 -0
- mudgym-0.3.0/src/mudgym/envs/fields/rawbytes.py +52 -0
- mudgym-0.3.0/src/mudgym/envs/fields/superquicklook.py +244 -0
- mudgym-0.3.0/src/mudgym/envs/registration.py +30 -0
- mudgym-0.3.0/src/mudgym/envs/specs.py +56 -0
- mudgym-0.3.0/src/mudgym/envs/validation.py +30 -0
- mudgym-0.3.0/src/mudgym/envs/vector.py +43 -0
- mudgym-0.3.0/src/mudgym/envs/zoo.py +229 -0
- mudgym-0.3.0/src/mudgym/featurizers/ansi.py +25 -0
- mudgym-0.3.0/src/mudgym/featurizers/persona_names.py +32 -0
- mudgym-0.3.0/src/mudgym/featurizers/points.py +57 -0
- mudgym-0.3.0/src/mudgym/featurizers/quickscore.py +44 -0
- mudgym-0.3.0/src/mudgym/featurizers/responses.py +114 -0
- mudgym-0.3.0/src/mudgym/featurizers/strings.py +58 -0
- mudgym-0.3.0/src/mudgym/logs.py +273 -0
- mudgym-0.3.0/src/mudgym/notebooks/__init__.py +39 -0
- mudgym-0.3.0/src/mudgym/notebooks/frames.py +378 -0
- mudgym-0.3.0/src/mudgym/notebooks/panels.py +144 -0
- mudgym-0.3.0/src/mudgym/notebooks/room_map.py +391 -0
- mudgym-0.3.0/src/mudgym/notebooks/style.py +65 -0
- mudgym-0.3.0/src/mudgym/notebooks/tables.py +177 -0
- mudgym-0.3.0/src/mudgym/session.py +247 -0
mudgym-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rolo Mawlabaux
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mudgym-0.3.0/NOTICE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
MudGym
|
|
2
|
+
Copyright (c) 2026 Rolo Mawlabaux
|
|
3
|
+
|
|
4
|
+
The Python code and tooling are made available under the MIT License (see
|
|
5
|
+
LICENSE).
|
|
6
|
+
|
|
7
|
+
Permission to use the MUD2 game for research purposes has been provided by
|
|
8
|
+
Richard Bartle, with kind thanks. The MUD2 game, name, source code, and
|
|
9
|
+
associated story remain the intellectual property of Richard Bartle and
|
|
10
|
+
Roy Trubshaw and may not be used for commercial purposes.
|
|
11
|
+
|
|
12
|
+
MudGym runs the MUD2 game inside the ghcr.io/rolo/mudgym Docker image; the
|
|
13
|
+
image is pulled at runtime and is not part of this distribution.
|
mudgym-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mudgym
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Reinforcement learning environment for MUD2.
|
|
5
|
+
Keywords: reinforcement-learning,gymnasium,pettingzoo,multi-agent,mud,mud2,text-adventure,llm-agents
|
|
6
|
+
Author: Rolo Mawlabaux
|
|
7
|
+
Author-email: Rolo Mawlabaux <rolo@kingrolo.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: NOTICE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Operating System :: POSIX
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Games/Entertainment :: Multi-User Dungeons (MUD)
|
|
21
|
+
Requires-Dist: ansi2html>=1.9.2
|
|
22
|
+
Requires-Dist: faker>=37.12.0
|
|
23
|
+
Requires-Dist: gymnasium>=1.2.2
|
|
24
|
+
Requires-Dist: pexpect>=4.9.0
|
|
25
|
+
Requires-Dist: structlog>=25.5.0
|
|
26
|
+
Requires-Dist: numpy>=2.3.4
|
|
27
|
+
Requires-Dist: pettingzoo>=1.25.0
|
|
28
|
+
Requires-Python: >=3.13
|
|
29
|
+
Project-URL: Homepage, https://rolo.github.io/mudgym/
|
|
30
|
+
Project-URL: Documentation, https://rolo.github.io/mudgym/
|
|
31
|
+
Project-URL: Repository, https://github.com/rolo/mudgym
|
|
32
|
+
Project-URL: Issues, https://github.com/rolo/mudgym/issues
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# MudGym
|
|
36
|
+
|
|
37
|
+
A Gymnasium reinforcement learning environment for MUD2. One of the first online multiplayer text adventure games.
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
A Docker engine is required to run the game. The image `ghcr.io/rolo/mudgym` is pulled automatically on first use.
|
|
42
|
+
|
|
43
|
+
Install MudGym from PyPI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv add mudgym
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from mudgym import make_env
|
|
53
|
+
|
|
54
|
+
env = make_env(observation="parsed")
|
|
55
|
+
observation, info = env.reset()
|
|
56
|
+
observation, reward, terminated, truncated, info = env.step("howl")
|
|
57
|
+
print(observation["room_name"], reward)
|
|
58
|
+
env.close()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Docs
|
|
62
|
+
|
|
63
|
+
See the [docs](https://rolo.github.io/mudgym/) for more examples.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
The Python code and tooling in this repository are made available under the MIT License.
|
|
68
|
+
|
|
69
|
+
Permission to use the MUD2 game for research purposes has been provided by Richard Bartle, with kind thanks. The MUD2 game, name, source code, and associated story remain the intellectual property of Richard Bartle and Roy Trubshaw and may not be used for commercial purposes.
|
mudgym-0.3.0/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# MudGym
|
|
2
|
+
|
|
3
|
+
A Gymnasium reinforcement learning environment for MUD2. One of the first online multiplayer text adventure games.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
A Docker engine is required to run the game. The image `ghcr.io/rolo/mudgym` is pulled automatically on first use.
|
|
8
|
+
|
|
9
|
+
Install MudGym from PyPI:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv add mudgym
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from mudgym import make_env
|
|
19
|
+
|
|
20
|
+
env = make_env(observation="parsed")
|
|
21
|
+
observation, info = env.reset()
|
|
22
|
+
observation, reward, terminated, truncated, info = env.step("howl")
|
|
23
|
+
print(observation["room_name"], reward)
|
|
24
|
+
env.close()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Docs
|
|
28
|
+
|
|
29
|
+
See the [docs](https://rolo.github.io/mudgym/) for more examples.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
The Python code and tooling in this repository are made available under the MIT License.
|
|
34
|
+
|
|
35
|
+
Permission to use the MUD2 game for research purposes has been provided by Richard Bartle, with kind thanks. The MUD2 game, name, source code, and associated story remain the intellectual property of Richard Bartle and Roy Trubshaw and may not be used for commercial purposes.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mudgym"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Reinforcement learning environment for MUD2."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = [
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"NOTICE",
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
keywords = [
|
|
13
|
+
"reinforcement-learning",
|
|
14
|
+
"gymnasium",
|
|
15
|
+
"pettingzoo",
|
|
16
|
+
"multi-agent",
|
|
17
|
+
"mud",
|
|
18
|
+
"mud2",
|
|
19
|
+
"text-adventure",
|
|
20
|
+
"llm-agents",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
30
|
+
"Operating System :: POSIX",
|
|
31
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
32
|
+
"Topic :: Games/Entertainment :: Multi-User Dungeons (MUD)",
|
|
33
|
+
]
|
|
34
|
+
dependencies = [
|
|
35
|
+
"ansi2html>=1.9.2",
|
|
36
|
+
"faker>=37.12.0",
|
|
37
|
+
"gymnasium>=1.2.2",
|
|
38
|
+
"pexpect>=4.9.0",
|
|
39
|
+
"structlog>=25.5.0",
|
|
40
|
+
"numpy>=2.3.4",
|
|
41
|
+
"pettingzoo>=1.25.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[[project.authors]]
|
|
45
|
+
name = "Rolo Mawlabaux"
|
|
46
|
+
email = "rolo@kingrolo.com"
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://rolo.github.io/mudgym/"
|
|
50
|
+
Documentation = "https://rolo.github.io/mudgym/"
|
|
51
|
+
Repository = "https://github.com/rolo/mudgym"
|
|
52
|
+
Issues = "https://github.com/rolo/mudgym/issues"
|
|
53
|
+
|
|
54
|
+
[dependency-groups]
|
|
55
|
+
docs = [
|
|
56
|
+
"zensical>=0.0.44",
|
|
57
|
+
"mkdocstrings-python>=2.0.4",
|
|
58
|
+
"markdown-exec>=1.12.3",
|
|
59
|
+
]
|
|
60
|
+
dev = [
|
|
61
|
+
"ruff>=0.14.3",
|
|
62
|
+
"pytest>=8.0.0",
|
|
63
|
+
"pytest-cov>=6.0.0",
|
|
64
|
+
"pytest-subtests>=0.15.0",
|
|
65
|
+
"ipdb>=0.13.13",
|
|
66
|
+
"tabulate>=0.10.0",
|
|
67
|
+
{ include-group = "docs" },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[build-system]
|
|
71
|
+
requires = ["uv_build>=0.9.7,<0.10.0"]
|
|
72
|
+
build-backend = "uv_build"
|
|
73
|
+
|
|
74
|
+
[tool.uv.build-backend]
|
|
75
|
+
source-exclude = [
|
|
76
|
+
"**/tests",
|
|
77
|
+
"**/tests/**",
|
|
78
|
+
]
|
|
79
|
+
wheel-exclude = [
|
|
80
|
+
"**/tests",
|
|
81
|
+
"**/tests/**",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
[tool.ruff]
|
|
85
|
+
line-length = 120
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint]
|
|
88
|
+
extend-select = [
|
|
89
|
+
"PLC0415",
|
|
90
|
+
"I",
|
|
91
|
+
"UP",
|
|
92
|
+
"SIM",
|
|
93
|
+
"C4",
|
|
94
|
+
"PIE",
|
|
95
|
+
"RET",
|
|
96
|
+
]
|
|
97
|
+
ignore = ["SIM300"]
|
|
98
|
+
|
|
99
|
+
[tool.black]
|
|
100
|
+
line-length = 120
|
|
101
|
+
|
|
102
|
+
[tool.coverage.run]
|
|
103
|
+
branch = true
|
|
104
|
+
source = ["src"]
|
|
105
|
+
relative_files = true
|
|
106
|
+
omit = ["*/tests/*"]
|
|
107
|
+
|
|
108
|
+
[tool.pytest.ini_options]
|
|
109
|
+
testpaths = [
|
|
110
|
+
"tests",
|
|
111
|
+
"src/mudgym",
|
|
112
|
+
]
|
|
113
|
+
log_level = "DEBUG"
|
|
114
|
+
log_cli_format = "%(asctime)s %(levelname)-7s %(message)s"
|
|
115
|
+
log_cli_date_format = "%H:%M:%S"
|
|
116
|
+
markers = ["soak: long-running live-game soak tests; excluded by default, run with -m soak"]
|
|
117
|
+
addopts = "-m 'not soak'"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mudgym"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Reinforcement learning environment for MUD2."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Rolo Mawlabaux", email = "rolo@kingrolo.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
keywords = [
|
|
13
|
+
"reinforcement-learning",
|
|
14
|
+
"gymnasium",
|
|
15
|
+
"pettingzoo",
|
|
16
|
+
"multi-agent",
|
|
17
|
+
"mud",
|
|
18
|
+
"mud2",
|
|
19
|
+
"text-adventure",
|
|
20
|
+
"llm-agents",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
30
|
+
# pexpect is POSIX-only, so the game connection does not work on Windows.
|
|
31
|
+
"Operating System :: POSIX",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
33
|
+
"Topic :: Games/Entertainment :: Multi-User Dungeons (MUD)",
|
|
34
|
+
]
|
|
35
|
+
dependencies = [
|
|
36
|
+
"ansi2html>=1.9.2",
|
|
37
|
+
"faker>=37.12.0",
|
|
38
|
+
"gymnasium>=1.2.2",
|
|
39
|
+
"pexpect>=4.9.0",
|
|
40
|
+
"structlog>=25.5.0",
|
|
41
|
+
"numpy>=2.3.4",
|
|
42
|
+
"pettingzoo>=1.25.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://rolo.github.io/mudgym/"
|
|
47
|
+
Documentation = "https://rolo.github.io/mudgym/"
|
|
48
|
+
Repository = "https://github.com/rolo/mudgym"
|
|
49
|
+
Issues = "https://github.com/rolo/mudgym/issues"
|
|
50
|
+
|
|
51
|
+
[dependency-groups]
|
|
52
|
+
docs = [
|
|
53
|
+
"zensical>=0.0.44",
|
|
54
|
+
"mkdocstrings-python>=2.0.4",
|
|
55
|
+
"markdown-exec>=1.12.3",
|
|
56
|
+
]
|
|
57
|
+
dev = [
|
|
58
|
+
"ruff>=0.14.3",
|
|
59
|
+
"pytest>=8.0.0",
|
|
60
|
+
"pytest-cov>=6.0.0",
|
|
61
|
+
"pytest-subtests>=0.15.0",
|
|
62
|
+
"ipdb>=0.13.13",
|
|
63
|
+
"tabulate>=0.10.0",
|
|
64
|
+
{include-group = "docs"},
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[build-system]
|
|
68
|
+
requires = ["uv_build>=0.9.7,<0.10.0"]
|
|
69
|
+
build-backend = "uv_build"
|
|
70
|
+
|
|
71
|
+
[tool.uv.build-backend]
|
|
72
|
+
# leave test modules out of the published artifacts
|
|
73
|
+
source-exclude = ["**/tests", "**/tests/**"]
|
|
74
|
+
wheel-exclude = ["**/tests", "**/tests/**"]
|
|
75
|
+
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
line-length = 120
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint]
|
|
80
|
+
extend-select = ["PLC0415", "I", "UP", "SIM", "C4", "PIE", "RET"]
|
|
81
|
+
# assertions read better as expected-then-actual, which SIM300 calls a Yoda condition
|
|
82
|
+
ignore = ["SIM300"]
|
|
83
|
+
|
|
84
|
+
# added to prevent tools which like to auto format with black
|
|
85
|
+
[tool.black]
|
|
86
|
+
line-length = 120
|
|
87
|
+
|
|
88
|
+
[tool.coverage.run]
|
|
89
|
+
branch = true
|
|
90
|
+
source = ["src"]
|
|
91
|
+
relative_files = true
|
|
92
|
+
omit = [
|
|
93
|
+
"*/tests/*",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[tool.pytest.ini_options]
|
|
97
|
+
testpaths = ["tests", "src/mudgym"]
|
|
98
|
+
# capture debug logs so a failing test's report includes the full wire history
|
|
99
|
+
log_level = "DEBUG"
|
|
100
|
+
log_cli_format = "%(asctime)s %(levelname)-7s %(message)s"
|
|
101
|
+
log_cli_date_format = "%H:%M:%S"
|
|
102
|
+
markers = [
|
|
103
|
+
"soak: long-running live-game soak tests; excluded by default, run with -m soak",
|
|
104
|
+
]
|
|
105
|
+
addopts = "-m 'not soak'"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MudGym: a reinforcement learning environment for MUD2.
|
|
3
|
+
|
|
4
|
+
Importing this package registers the Gymnasium env IDs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
|
|
9
|
+
from mudgym.envs.factory import make_env, make_parallel_env, make_vector_env
|
|
10
|
+
from mudgym.envs.registration import register_envs as _register_envs
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = version("mudgym")
|
|
14
|
+
except PackageNotFoundError: # running from a source tree that was never installed
|
|
15
|
+
__version__ = "0.0.0.dev0"
|
|
16
|
+
|
|
17
|
+
_register_envs()
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"__version__",
|
|
21
|
+
"make_env",
|
|
22
|
+
"make_parallel_env",
|
|
23
|
+
"make_vector_env",
|
|
24
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Public action vocabulary helpers."""
|
|
2
|
+
|
|
3
|
+
from mudgym.db.directions import DIRECTION_INDEX_BY_NAME, DIRECTIONS
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def direction_index(direction: str) -> int:
|
|
7
|
+
"""Return the zero-based action and exit-mask index for a direction."""
|
|
8
|
+
return DIRECTION_INDEX_BY_NAME[direction]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = ["DIRECTIONS", "direction_index"]
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
DOCKER_IMAGE = os.getenv("DOCKER_IMAGE", "ghcr.io/rolo/mudgym:v0.2.3")
|
|
4
|
+
CONTAINER_PREFIX = "mudgym"
|
|
5
|
+
DEFAULT_DOCKER_EXEC_CONTAINER_NAME = "mud2-boot"
|
|
6
|
+
|
|
7
|
+
# connection defaults
|
|
8
|
+
DEFAULT_ACCOUNT_ID = "W00000001"
|
|
9
|
+
DEFAULT_PASSWORD = "password"
|
|
10
|
+
|
|
11
|
+
# first listed in AVAILABLE_CONNECTIONS is default
|
|
12
|
+
AVAILABLE_CONNECTIONS = os.getenv("AVAILABLE_CONNECTIONS", "docker_run,docker_exec")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def configured_docker_exec_container_name() -> str:
|
|
16
|
+
"""Return the shared-container name configured for this process."""
|
|
17
|
+
return os.getenv("MUDGYM_DOCKER_EXEC_CONTAINER_NAME", DEFAULT_DOCKER_EXEC_CONTAINER_NAME)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Callable, Sequence
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pexpect
|
|
6
|
+
|
|
7
|
+
from mudgym.connections.prompts import FINAL_COMMAND_MARKER, PromptSpec, State
|
|
8
|
+
from mudgym.connections.state_machine import ConnectionState
|
|
9
|
+
from mudgym.logs import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MudConnection:
|
|
15
|
+
"""
|
|
16
|
+
Base class for managing connections to MUD2 instances.
|
|
17
|
+
|
|
18
|
+
The connection lifecycle is managed by the state machine, this class wraps the state machine and allows for
|
|
19
|
+
different types of transport whilst exposing a (hopefully) easier to deal with API.
|
|
20
|
+
|
|
21
|
+
Lifecycle:
|
|
22
|
+
- reset() -> Get us to the TEA_SIPPED state, ready for an episode to begin.
|
|
23
|
+
- send_command(command: str) -> tuple[bytes, bool, bool, dict] -> Send a command to the game, receiving the
|
|
24
|
+
raw response bytes, terminated and incomplete flags and some debug info.
|
|
25
|
+
- close() -> Close the connection, terminating the child process. Typically you would use reset() instead if you
|
|
26
|
+
are going to want to reuse the connection to start a new episode (for connections that support it).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# initial prompt we expect to see - subclasses can override
|
|
30
|
+
initial_prompt: PromptSpec | None = None
|
|
31
|
+
|
|
32
|
+
# the end of turn marker closing each step's read window: the pattern identifying the response of the batch's
|
|
33
|
+
# final command. This class attribute is the protocol's one declared default (fei's ======== divider), which
|
|
34
|
+
# keeps a bare connection usable on its own; the session overrides it per instance with whatever marker the
|
|
35
|
+
# env's batch actually ends with.
|
|
36
|
+
end_of_turn_marker: re.Pattern = FINAL_COMMAND_MARKER
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
account_id: str = "",
|
|
42
|
+
password: str = "",
|
|
43
|
+
persona_slot: int | None = None,
|
|
44
|
+
db_slot: int | None = None,
|
|
45
|
+
name_generator: Callable[[], str] | None = None,
|
|
46
|
+
):
|
|
47
|
+
# I'm not convinced we actually need many of these anymore, but they're here for now.
|
|
48
|
+
self.account_id = account_id
|
|
49
|
+
self.password = password
|
|
50
|
+
self.persona_slot = persona_slot
|
|
51
|
+
self.db_slot = db_slot
|
|
52
|
+
self.name_generator = name_generator
|
|
53
|
+
|
|
54
|
+
# our state machine instance, that does most of the heavy lifting
|
|
55
|
+
self.sm: ConnectionState | None = None
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def is_available(cls) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Check if the connection is available to use in the current environment. Subclasses can override this method to
|
|
61
|
+
perform a check specific to the connection type. This is to help with experimenting with different connections
|
|
62
|
+
types.
|
|
63
|
+
"""
|
|
64
|
+
logger.debug("connection.is_available.default", connection_class=cls.__name__)
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
def spawn(self) -> pexpect.spawn:
|
|
68
|
+
"""
|
|
69
|
+
The method that does the actual connecting by spawning and returning our child process.
|
|
70
|
+
"""
|
|
71
|
+
return pexpect.spawn(
|
|
72
|
+
self.command[0],
|
|
73
|
+
self.command[1:] if len(self.command) > 1 else [],
|
|
74
|
+
encoding=None,
|
|
75
|
+
use_poll=True, # poll() instead of select() to avoid FD_SETSIZE limit
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def reset(self) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Resets the connection to be ready to start a new episode (TEA_SIPPED state).
|
|
81
|
+
|
|
82
|
+
This tells us the `MudConnection` is ready but the `MudEnv` has its own `reset()` steps afterwards that does
|
|
83
|
+
episode related things that don't make sense here, like issuing commands to set up the initial environment state
|
|
84
|
+
(eg, score) and running start of episode autocommands and taking the northwards step outside of the tearoom.
|
|
85
|
+
|
|
86
|
+
I can imagine a situation with multiple `MudConnection`s waiting on each other after `reset()` to be ready so it
|
|
87
|
+
seemed negligent to leave agents hanging around outside of the sanctity of the Tearoom where they might get
|
|
88
|
+
attacked by mobiles or something.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
# do we need to respawn the process or can we reuse via some menu choices?
|
|
92
|
+
needs_respawn = self.sm is None or not self.sm.isalive()
|
|
93
|
+
logger.debug(
|
|
94
|
+
"connection.reset.start",
|
|
95
|
+
sm_state=self.sm.state.name if self.sm is not None else None,
|
|
96
|
+
needs_respawn=needs_respawn,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if self.sm is not None and self.sm.isalive():
|
|
100
|
+
# reset-quit: leave The Land but stay in the mudlogin menu if our connection type
|
|
101
|
+
# supports that (ie, not a quicklogin, which exits to DEAD)
|
|
102
|
+
self.sm.quit()
|
|
103
|
+
|
|
104
|
+
if self.sm.state == State.DEAD:
|
|
105
|
+
needs_respawn = True
|
|
106
|
+
|
|
107
|
+
if needs_respawn:
|
|
108
|
+
logger.debug("connection.reset.spawn")
|
|
109
|
+
child = self.spawn()
|
|
110
|
+
self.sm = ConnectionState(
|
|
111
|
+
child=child,
|
|
112
|
+
account_id=self.account_id,
|
|
113
|
+
password=self.password,
|
|
114
|
+
persona_slot=self.persona_slot,
|
|
115
|
+
db_slot=self.db_slot,
|
|
116
|
+
name_generator=self.name_generator,
|
|
117
|
+
initial_prompt=self.initial_prompt,
|
|
118
|
+
end_of_turn_marker=self.end_of_turn_marker,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# OPTION -> persona selection/creation -> TEAROOM -> sip tea -> TEA_SIPPED
|
|
122
|
+
logger.debug("connection.reset.continue_until_tea", sm_state=self.sm.state.name)
|
|
123
|
+
self.sm.continue_until(State.TEA_SIPPED)
|
|
124
|
+
logger.debug(
|
|
125
|
+
"connection.reset.complete",
|
|
126
|
+
sm_state=self.sm.state.name,
|
|
127
|
+
last_prompt=self.sm.last_prompt.name if self.sm.last_prompt else None,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def send_command(self, command: str | Sequence[str]) -> tuple[bytes, bool, bool, dict[str, Any]]:
|
|
131
|
+
"""
|
|
132
|
+
Send a command batch (one wire line, or several when the caller split it) and return the
|
|
133
|
+
raw response bytes, terminated/incomplete flags, and debug info.
|
|
134
|
+
"""
|
|
135
|
+
if self.sm is None:
|
|
136
|
+
raise RuntimeError("Connection has not been reset, call reset() first.")
|
|
137
|
+
lines = [command] if isinstance(command, str) else list(command)
|
|
138
|
+
if not lines:
|
|
139
|
+
raise ValueError("send_command requires at least one command line; got an empty batch")
|
|
140
|
+
return self.sm.send_command(lines)
|
|
141
|
+
|
|
142
|
+
def close(self):
|
|
143
|
+
if self.sm is None:
|
|
144
|
+
return
|
|
145
|
+
try:
|
|
146
|
+
if self.sm.isalive():
|
|
147
|
+
self.sm.quit()
|
|
148
|
+
finally:
|
|
149
|
+
self.sm.close()
|
|
150
|
+
self.sm = None
|