Add bare echoing telnet server
This commit is contained in:
parent
27ec0a8a22
commit
a83248b387
7 changed files with 176 additions and 0 deletions
3
justfile
3
justfile
|
|
@ -9,3 +9,6 @@ test:
|
||||||
uv run pytest
|
uv run pytest
|
||||||
|
|
||||||
check: lint typecheck test
|
check: lint typecheck test
|
||||||
|
|
||||||
|
run:
|
||||||
|
uv run python -m mudlib
|
||||||
|
|
|
||||||
3
mud.tin
Normal file
3
mud.tin
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#NOP TinTin++ config for connecting to the MUD server
|
||||||
|
#split 0 1
|
||||||
|
#session mud localhost 6789
|
||||||
|
|
@ -12,6 +12,7 @@ dev = [
|
||||||
"ruff",
|
"ruff",
|
||||||
"pyright",
|
"pyright",
|
||||||
"pytest",
|
"pytest",
|
||||||
|
"pytest-asyncio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
|
||||||
8
src/mudlib/__main__.py
Normal file
8
src/mudlib/__main__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
"""Main entry point for running the MUD server."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from mudlib.server import run_server
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(run_server())
|
||||||
54
src/mudlib/server.py
Normal file
54
src/mudlib/server.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""Telnet server for the MUD."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import telnetlib3
|
||||||
|
|
||||||
|
PORT = 6789
|
||||||
|
|
||||||
|
|
||||||
|
async def shell(
|
||||||
|
reader: telnetlib3.TelnetReader | telnetlib3.TelnetReaderUnicode,
|
||||||
|
writer: telnetlib3.TelnetWriter | telnetlib3.TelnetWriterUnicode,
|
||||||
|
) -> None:
|
||||||
|
"""Shell callback that greets the player and echoes their input."""
|
||||||
|
# Cast to unicode variants since we're using encoding="utf8"
|
||||||
|
_reader = cast(telnetlib3.TelnetReaderUnicode, reader)
|
||||||
|
_writer = cast(telnetlib3.TelnetWriterUnicode, writer)
|
||||||
|
|
||||||
|
_writer.write("Welcome to the MUD!\r\n")
|
||||||
|
_writer.write("Type 'quit' to disconnect.\r\n\r\n")
|
||||||
|
await _writer.drain()
|
||||||
|
|
||||||
|
while not _writer.is_closing():
|
||||||
|
_writer.write("mud> ")
|
||||||
|
await _writer.drain()
|
||||||
|
|
||||||
|
inp = await _reader.readline()
|
||||||
|
if not inp:
|
||||||
|
break
|
||||||
|
|
||||||
|
command = inp.strip()
|
||||||
|
if command == "quit":
|
||||||
|
_writer.write("Goodbye!\r\n")
|
||||||
|
break
|
||||||
|
|
||||||
|
_writer.write(f"You typed: {command}\r\n")
|
||||||
|
|
||||||
|
_writer.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_server() -> None:
|
||||||
|
"""Start the MUD telnet server."""
|
||||||
|
server = await telnetlib3.create_server(host="127.0.0.1", port=PORT, shell=shell)
|
||||||
|
print(f"MUD server running on 127.0.0.1:{PORT}")
|
||||||
|
print("Connect with: telnet 127.0.0.1 6789")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(3600)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down...")
|
||||||
|
server.close()
|
||||||
|
await server.wait_closed()
|
||||||
92
tests/test_server.py
Normal file
92
tests/test_server.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
"""Tests for the server module."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mudlib import server
|
||||||
|
|
||||||
|
|
||||||
|
def test_port_constant():
|
||||||
|
"""Test that PORT is defined correctly."""
|
||||||
|
assert server.PORT == 6789
|
||||||
|
assert isinstance(server.PORT, int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shell_exists():
|
||||||
|
"""Test that the shell callback exists and is callable."""
|
||||||
|
assert callable(server.shell)
|
||||||
|
assert asyncio.iscoroutinefunction(server.shell)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_server_exists():
|
||||||
|
"""Test that run_server exists and is callable."""
|
||||||
|
assert callable(server.run_server)
|
||||||
|
assert asyncio.iscoroutinefunction(server.run_server)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_shell_greets_and_echoes():
|
||||||
|
"""Test that shell greets the player and echoes input."""
|
||||||
|
reader = AsyncMock()
|
||||||
|
writer = MagicMock()
|
||||||
|
writer.is_closing.return_value = False
|
||||||
|
writer.drain = AsyncMock()
|
||||||
|
writer.close = MagicMock()
|
||||||
|
|
||||||
|
# Simulate user typing "hello" then "quit"
|
||||||
|
reader.readline.side_effect = ["hello\r\n", "quit\r\n"]
|
||||||
|
|
||||||
|
await server.shell(reader, writer)
|
||||||
|
|
||||||
|
# Check that welcome message was written
|
||||||
|
calls = [str(call) for call in writer.write.call_args_list]
|
||||||
|
assert any("Welcome" in call for call in calls)
|
||||||
|
|
||||||
|
# Check that input was echoed
|
||||||
|
assert any("hello" in call for call in calls)
|
||||||
|
|
||||||
|
# Check that goodbye was written
|
||||||
|
assert any("Goodbye" in call for call in calls)
|
||||||
|
|
||||||
|
# Check that close was called
|
||||||
|
writer.close.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_shell_handles_empty_input():
|
||||||
|
"""Test that shell handles EOF gracefully."""
|
||||||
|
reader = AsyncMock()
|
||||||
|
writer = MagicMock()
|
||||||
|
writer.is_closing.return_value = False
|
||||||
|
writer.drain = AsyncMock()
|
||||||
|
writer.close = MagicMock()
|
||||||
|
|
||||||
|
# Simulate EOF
|
||||||
|
reader.readline.return_value = ""
|
||||||
|
|
||||||
|
await server.shell(reader, writer)
|
||||||
|
|
||||||
|
# Should close cleanly
|
||||||
|
writer.close.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_shell_handles_quit():
|
||||||
|
"""Test that shell exits on quit command."""
|
||||||
|
reader = AsyncMock()
|
||||||
|
writer = MagicMock()
|
||||||
|
writer.is_closing.return_value = False
|
||||||
|
writer.drain = AsyncMock()
|
||||||
|
writer.close = MagicMock()
|
||||||
|
|
||||||
|
reader.readline.return_value = "quit\r\n"
|
||||||
|
|
||||||
|
await server.shell(reader, writer)
|
||||||
|
|
||||||
|
# Check that goodbye was written
|
||||||
|
calls = [str(call) for call in writer.write.call_args_list]
|
||||||
|
assert any("Goodbye" in call for call in calls)
|
||||||
|
|
||||||
|
writer.close.assert_called_once()
|
||||||
15
uv.lock
15
uv.lock
|
|
@ -32,6 +32,7 @@ dependencies = [
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pyright" },
|
{ name = "pyright" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -42,6 +43,7 @@ requires-dist = [{ name = "telnetlib3" }]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pyright" },
|
{ name = "pyright" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -110,6 +112,19 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue