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.
Files changed (59) hide show
  1. mudgym-0.3.0/LICENSE +21 -0
  2. mudgym-0.3.0/NOTICE +13 -0
  3. mudgym-0.3.0/PKG-INFO +69 -0
  4. mudgym-0.3.0/README.md +35 -0
  5. mudgym-0.3.0/pyproject.toml +117 -0
  6. mudgym-0.3.0/pyproject.toml.orig +105 -0
  7. mudgym-0.3.0/src/mudgym/__init__.py +24 -0
  8. mudgym-0.3.0/src/mudgym/actions.py +11 -0
  9. mudgym-0.3.0/src/mudgym/connections/__init__.py +0 -0
  10. mudgym-0.3.0/src/mudgym/connections/config.py +17 -0
  11. mudgym-0.3.0/src/mudgym/connections/connection.py +150 -0
  12. mudgym-0.3.0/src/mudgym/connections/docker_exec.py +152 -0
  13. mudgym-0.3.0/src/mudgym/connections/docker_image.py +98 -0
  14. mudgym-0.3.0/src/mudgym/connections/docker_run.py +95 -0
  15. mudgym-0.3.0/src/mudgym/connections/persona.py +47 -0
  16. mudgym-0.3.0/src/mudgym/connections/prompts.py +298 -0
  17. mudgym-0.3.0/src/mudgym/connections/provider.py +250 -0
  18. mudgym-0.3.0/src/mudgym/connections/recording.py +218 -0
  19. mudgym-0.3.0/src/mudgym/connections/registry.py +81 -0
  20. mudgym-0.3.0/src/mudgym/connections/state_machine.py +533 -0
  21. mudgym-0.3.0/src/mudgym/connections/termination.py +47 -0
  22. mudgym-0.3.0/src/mudgym/connections/transitions.py +234 -0
  23. mudgym-0.3.0/src/mudgym/db/directions.py +38 -0
  24. mudgym-0.3.0/src/mudgym/db/index.py +122 -0
  25. mudgym-0.3.0/src/mudgym/db/levels.py +68 -0
  26. mudgym-0.3.0/src/mudgym/db/rooms.py +1156 -0
  27. mudgym-0.3.0/src/mudgym/db/weather.py +15 -0
  28. mudgym-0.3.0/src/mudgym/envs/__init__.py +0 -0
  29. mudgym-0.3.0/src/mudgym/envs/actions/__init__.py +0 -0
  30. mudgym-0.3.0/src/mudgym/envs/actions/discrete.py +53 -0
  31. mudgym-0.3.0/src/mudgym/envs/env.py +375 -0
  32. mudgym-0.3.0/src/mudgym/envs/factory.py +273 -0
  33. mudgym-0.3.0/src/mudgym/envs/fields/__init__.py +31 -0
  34. mudgym-0.3.0/src/mudgym/envs/fields/feinventory.py +75 -0
  35. mudgym-0.3.0/src/mudgym/envs/fields/fescore.py +140 -0
  36. mudgym-0.3.0/src/mudgym/envs/fields/fexits.py +92 -0
  37. mudgym-0.3.0/src/mudgym/envs/fields/field.py +126 -0
  38. mudgym-0.3.0/src/mudgym/envs/fields/mgcheats.py +113 -0
  39. mudgym-0.3.0/src/mudgym/envs/fields/rawbytes.py +52 -0
  40. mudgym-0.3.0/src/mudgym/envs/fields/superquicklook.py +244 -0
  41. mudgym-0.3.0/src/mudgym/envs/registration.py +30 -0
  42. mudgym-0.3.0/src/mudgym/envs/specs.py +56 -0
  43. mudgym-0.3.0/src/mudgym/envs/validation.py +30 -0
  44. mudgym-0.3.0/src/mudgym/envs/vector.py +43 -0
  45. mudgym-0.3.0/src/mudgym/envs/zoo.py +229 -0
  46. mudgym-0.3.0/src/mudgym/featurizers/ansi.py +25 -0
  47. mudgym-0.3.0/src/mudgym/featurizers/persona_names.py +32 -0
  48. mudgym-0.3.0/src/mudgym/featurizers/points.py +57 -0
  49. mudgym-0.3.0/src/mudgym/featurizers/quickscore.py +44 -0
  50. mudgym-0.3.0/src/mudgym/featurizers/responses.py +114 -0
  51. mudgym-0.3.0/src/mudgym/featurizers/strings.py +58 -0
  52. mudgym-0.3.0/src/mudgym/logs.py +273 -0
  53. mudgym-0.3.0/src/mudgym/notebooks/__init__.py +39 -0
  54. mudgym-0.3.0/src/mudgym/notebooks/frames.py +378 -0
  55. mudgym-0.3.0/src/mudgym/notebooks/panels.py +144 -0
  56. mudgym-0.3.0/src/mudgym/notebooks/room_map.py +391 -0
  57. mudgym-0.3.0/src/mudgym/notebooks/style.py +65 -0
  58. mudgym-0.3.0/src/mudgym/notebooks/tables.py +177 -0
  59. 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