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
|
||||
|
||||
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",
|
||||
"pyright",
|
||||
"pytest",
|
||||
"pytest-asyncio",
|
||||
]
|
||||
|
||||
[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 = [
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ requires-dist = [{ name = "telnetlib3" }]
|
|||
dev = [
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "ruff"
|
||||
version = "0.15.0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue