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)
112 lines
4.0 KiB
Python
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]
|