From 5f3ea6a781ebbc1a6bd47ebc6ba8163c59b3bdad Mon Sep 17 00:00:00 2001 From: Daniel Eder Date: Sat, 27 Sep 2025 08:02:50 +0200 Subject: [PATCH] feat: support agent selection via environment variables --- .env | 3 ++- .gitignore | 5 +++- README.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ agent.py | 15 ++++++++++++ app.py | 44 +++++++++++++++++++++++++++++++-- 5 files changed, 134 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 15b125c..45c1ba4 100644 --- a/.env +++ b/.env @@ -4,4 +4,5 @@ MISTLE_USER=mistle MISTLE_PASSWORD=sl-mudbot MISTLE_LOGIN_PROMPT=Wie heisst Du denn ("neu" fuer neuen Spieler) ? MISTLE_EXIT_COMMAND=schlaf ein -MISTLE_AGENT_MODE=true \ No newline at end of file +MISTLE_AGENT_MODE=true +MISTLE_AGENT=simple \ No newline at end of file diff --git a/.gitignore b/.gitignore index dbd1988..08057bb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ wheels/ # Virtual environments .venv -.DS_STORE \ No newline at end of file +.DS_STORE + +# config +.env \ No newline at end of file diff --git a/README.md b/README.md index e69de29..6c3b0c6 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,71 @@ +# Mistle Mudbot + +Python-based Telnet helper for connecting to MUD servers, handling login flows, and optionally running an automated "agent" alongside interactive play. + +## Features + +- Lightweight wrapper (`TelnetClient`) around `telnetlib` with sane defaults and context-manager support. +- Loads credentials and connection settings from a local `.env` file. +- Interactive console session that mirrors server output and lets you type commands directly. +- Optional agent mode that runs on a background thread and can dispatch automated commands without blocking manual input. +- Pluggable agent selection via environment variables (built-in `SimpleAgent`, extendable with custom classes). + +## Requirements + +- Python 3.10+ +- A reachable Telnet-based MUD server + +## Quick Start + +1. Create a virtual environment and install dependencies: + + ```bash + pip install -e . + ``` + +2. Copy `.env.example` (if available) or create a `.env` file in the project root, then fill in the connection details described below. + +3. Run the client: + + ```bash + python app.py + ``` + +4. Type commands directly into the console. Press `Ctrl-C` to exit; the client will send any configured shutdown command to the MUD. + +## Environment Variables + +All variables can be placed in the `.env` file (one `KEY=value` per line) or provided through the shell environment. + +| Variable | Required | Description | +| --- | --- | --- | +| `MISTLE_HOST` | ✅ | DNS name or IP of the target MUD server. | +| `MISTLE_PORT` | ✅ | Telnet port number (will be cast to `int`). | +| `MISTLE_USER` | ❌ | Username or character name; sent automatically after login banner, if provided. | +| `MISTLE_PASSWORD` | ❌ | Password sent after the username. Leave blank for manual entry. | +| `MISTLE_LOGIN_PROMPT` | ❌ | Prompt string that signals the client to send credentials (e.g., `"Name:"`). When omitted, the client just waits for the initial banner. | +| `MISTLE_EXIT_COMMAND` | ❌ | Command issued during graceful shutdown (after pressing `Ctrl-C`). Useful for `quit`/`save` macros. | +| `MISTLE_AGENT_MODE` | ❌ | Enable agent thread when set to truthy values (`1`, `true`, `yes`, `on`). Defaults to interactive-only mode. | +| `MISTLE_AGENT` | ❌ | Select which agent class to instantiate when agent mode is active. Accepted values: `simple` (default), `explore` (requires `ExploreAgent` inside `agent.py`), or custom spec `module:ClassName`. | + +## Agent Development + +- Implement new agents by subclassing `agent.Agent` and overriding `observe()` and `decide()`. +- Register the agent by either: + - Adding the class to `agent.py` and referencing it in `MISTLE_AGENT` (e.g., `explore` if the class is named `ExploreAgent`). + - Placing the class elsewhere and configuring `MISTLE_AGENT` to `your_module:YourAgent`. +- `observe(output)` receives the latest server text; `decide()` returns the next command string or `None` to stay idle. +- Commands issued by the agent are throttled to one per second so manual commands can still interleave smoothly. + +## Danger Zone + +- The Telnet session runs until you interrupt it. Make sure the terminal is in a state where `Ctrl-C` is available. +- When adding new agents, guard any long-running logic to avoid blocking the agent thread. + +## Contributing + +Feel free to open issues or submit pull requests for additional MUD-specific helpers, new agents, or quality-of-life improvements. + +--- + +Happy MUDding! diff --git a/agent.py b/agent.py index 240baf4..989e833 100644 --- a/agent.py +++ b/agent.py @@ -30,3 +30,18 @@ class SimpleAgent(Agent): def decide(self) -> Optional[str]: return self.default_command + + +@dataclass +class ExploreAgent(Agent): + """Very small stateful agent that decides which command to run next.""" + + default_command: str = "untersuche boden" + last_output: str = field(default="", init=False) + + def observe(self, output: str) -> None: + if output: + self.last_output = output + + def decide(self) -> Optional[str]: + return self.default_command \ No newline at end of file diff --git a/app.py b/app.py index f2c0cb1..3c1fb92 100644 --- a/app.py +++ b/app.py @@ -2,9 +2,10 @@ import os import select import sys import time +from importlib import import_module from pathlib import Path from threading import Event, Lock, Thread -from typing import Optional +from typing import Optional, Type from agent import Agent, SimpleAgent from telnetclient import TelnetClient @@ -129,6 +130,44 @@ def require_env(key: str) -> str: return value +def build_agent(agent_spec: str) -> Agent: + """Instantiate an agent based on ``MISTLE_AGENT`` contents.""" + normalized = agent_spec.strip() + if not normalized: + return SimpleAgent() + + key = normalized.lower() + if key == "simple": + return SimpleAgent() + + if key == "explore": + try: + module = import_module("agent") + agent_cls = getattr(module, "ExploreAgent") + except AttributeError as exc: # pragma: no cover - optional dependency + raise RuntimeError("ExploreAgent is not available in agent module") from exc + return _instantiate_agent(agent_cls, normalized) + + if ":" in normalized: + module_name, class_name = normalized.split(":", 1) + if not module_name or not class_name: + raise RuntimeError("MISTLE_AGENT must be in 'module:ClassName' format") + module = import_module(module_name) + agent_cls = getattr(module, class_name) + return _instantiate_agent(agent_cls, normalized) + + raise RuntimeError(f"Unknown agent spec '{agent_spec}'.") + + +def _instantiate_agent(agent_cls: Type[Agent], agent_spec: str) -> Agent: + if not issubclass(agent_cls, Agent): + raise RuntimeError(f"{agent_spec} is not an Agent subclass") + try: + return agent_cls() + except TypeError as exc: + raise RuntimeError(f"Failed to instantiate {agent_spec}: {exc}") from exc + + def login( client: TelnetClient, *, @@ -233,6 +272,7 @@ def main() -> int: login_prompt = os.environ.get("MISTLE_LOGIN_PROMPT", "") exit_command = os.environ.get("MISTLE_EXIT_COMMAND", "") agent_mode = os.environ.get("MISTLE_AGENT_MODE", "").lower() in {"1", "true", "yes", "on"} + agent_spec = os.environ.get("MISTLE_AGENT", "") state = SessionState() stop_event = Event() @@ -249,7 +289,7 @@ def main() -> int: ) if agent_mode: - agent = SimpleAgent() + agent = build_agent(agent_spec) agent_thread = Thread( target=run_agent_loop, args=(client, state, agent, stop_event),