Files
truetype f85c27a742 Add pluggable parser system, perception loop, and project infrastructure
Phase 0 (housekeeping):
- Add .gitignore to protect credentials and runtime files
- Track all project source code, docs, and config

Phase 1 (perception loop):
- Implement pluggable parser architecture (BaseMudParser ABC + ParserFactory)
- Add DeadSoulsParser with regex for HP/MP/SP prompt, room descriptions,
  exits (multiple formats), NPC/item detection, and combat events
- Add OutputBuffer for accumulating text between prompts
- Update GameState with SP support and update_from_prompt/room/combat methods
- Rewire main loop to use parser + buffer for real-time game state updates
- Add reconnection logic to TelnetClient (retry with backoff)
- Remove old TextParser (replaced by pluggable system)
- Add mud_type config field for selecting parser at runtime
- Add comprehensive test suite (50 tests, all passing)
2026-02-08 19:08:59 +01:00

112 lines
4.0 KiB
Python

"""Tests for the OutputBuffer."""
import pytest
from core.parsers.buffer import OutputBuffer
from core.parsers.dead_souls import DeadSoulsParser
@pytest.fixture
def parser():
return DeadSoulsParser()
@pytest.fixture
def buffer(parser):
return OutputBuffer(parser)
class TestOutputBuffer:
def test_no_prompt_buffers_text(self, buffer):
"""Text without a prompt should be buffered, not returned."""
blocks = buffer.feed("You are in a dark room.\nA torch flickers.")
assert blocks == []
assert buffer.pending_lines == 2
def test_prompt_flushes_buffer(self, buffer):
"""When a prompt arrives, the buffered text is flushed."""
buffer.feed("Town Square\n\nYou are in the town square.\n")
blocks = buffer.feed("hp: 350/350 mp: 440/440 sp: 431/431 >")
# Should get 2 blocks: the accumulated room text + the prompt line
assert len(blocks) == 2
assert "Town Square" in blocks[0]
assert "hp:" in blocks[1]
assert buffer.pending_lines == 0
def test_multiple_prompts(self, buffer):
"""Two prompts in one feed should flush twice."""
text = (
"Room A\nhp: 100/100 mp: 50/50 sp: 75/75 >\n"
"Room B\nhp: 100/100 mp: 50/50 sp: 75/75 >\n"
)
blocks = buffer.feed(text)
# Expect: "Room A", prompt, "Room B", prompt
assert len(blocks) == 4
assert "Room A" in blocks[0]
assert "hp:" in blocks[1]
assert "Room B" in blocks[2]
assert "hp:" in blocks[3]
def test_flush_remaining(self, buffer):
"""flush() should return whatever is in the buffer."""
buffer.feed("Some text without prompt\nAnother line")
remaining = buffer.flush()
assert remaining is not None
assert "Some text" in remaining
assert buffer.pending_lines == 0
def test_flush_empty(self, buffer):
"""flush() on empty buffer returns None."""
assert buffer.flush() is None
def test_max_lines_safety(self, parser):
"""Buffer should auto-flush when exceeding max_lines."""
small_buffer = OutputBuffer(parser, max_lines=5)
# Feed 10 lines without a prompt
text = "\n".join(f"line {i}" for i in range(10))
blocks = small_buffer.feed(text)
# Should have flushed at least once due to exceeding 5 lines
assert len(blocks) >= 1
assert small_buffer.pending_lines < 5
def test_prompt_line_is_separate_block(self, buffer):
"""The prompt itself should be its own block, not mixed with room text."""
buffer.feed("Dark Forest\nTrees surround you.\n")
blocks = buffer.feed("hp: 200/300 mp: 100/200 sp: 150/200 >")
# blocks[0] = room text, blocks[1] = prompt
assert len(blocks) == 2
assert "hp:" not in blocks[0]
assert "hp:" in blocks[1]
class TestBufferWithRealGameOutput:
"""Integration-style tests using text patterns from real Dead Souls sessions."""
def test_login_then_room(self, buffer):
"""Simulate the login sequence followed by the first room."""
# Login messages (no prompt)
buffer.feed("Allowing login.\n")
# Then the game sends the prompt
blocks = buffer.feed("hp: 350/350 mp: 440/440 sp: 431/431 >")
assert len(blocks) == 2
assert "Allowing login." in blocks[0]
assert "hp: 350/350" in blocks[1]
def test_room_description_then_prompt(self, buffer):
"""Simulate receiving a room description followed by prompt."""
text = (
"Town Square\n"
"\n"
"You are standing in the town square.\n"
"There is one obvious exit: north.\n"
"A guard is standing here.\n"
"hp: 350/350 mp: 440/440 sp: 431/431 >\n"
)
blocks = buffer.feed(text)
# First block: room text, second: prompt
assert len(blocks) == 2
room_block = blocks[0]
assert "Town Square" in room_block
assert "guard" in room_block
assert "hp:" in blocks[1]